Skip to content

Commit a6c3cd4

Browse files
keithamusderdeka
andauthored
copy over some basics from WPT to get a semi-reasonable test suite (#9)
* copy over some basics from WPT to get a semi-reasonable test suite * Update package.json Co-authored-by: derdeka <derdeka@users.noreply.github.com> --------- Co-authored-by: derdeka <derdeka@users.noreply.github.com>
1 parent 39aec7d commit a6c3cd4

32 files changed

Lines changed: 11999 additions & 1 deletion

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"scripts": {
4141
"minify": "esbuild --bundle --minify index.js > observable.min.js",
4242
"prepublishOnly": "npm run minify",
43-
"test": "echo TODO! Run ./wpt serve --inject-script=../observables-polyfill/observable.js for now."
43+
"test": "echo TODO! Run ./wpt serve --inject-script=../observables-polyfill/observable.js for now.",
44+
"test-serve": "npx http-server -c1 -t0 --cors --gzip --brotli -o test ."
4445
},
4546
"devDependencies": {
4647
"esbuild": "^0.25.2"

test/index.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<script type="module">
5+
import { apply } from "../observable.js";
6+
apply();
7+
</script>
8+
<script defer src="./wpt/testharness.js"></script>
9+
<script defer src="./wpt/testharnessreport.js"></script>
10+
<script defer src="./wpt/observable-drop.any.js"></script>
11+
<script defer src="./wpt/observable-filter.any.js"></script>
12+
<script defer src="./wpt/observable-flatMap.any.js"></script>
13+
<script defer src="./wpt/observable-inspect.any.js"></script>
14+
<script defer src="./wpt/observable-reduce.any.js"></script>
15+
<script defer src="./wpt/observable-takeUntil.any.js"></script>
16+
<script defer src="./wpt/observable-catch.any.js"></script>
17+
<script defer src="./wpt/observable-event-target.any.js"></script>
18+
<script defer src="./wpt/observable-finally.any.js"></script>
19+
<script defer src="./wpt/observable-forEach.any.js"></script>
20+
<script defer src="./wpt/observable-last.any.js"></script>
21+
<script defer src="./wpt/observable-some.any.js"></script>
22+
<script defer src="./wpt/observable-takeUntil.window.js"></script>
23+
<script defer src="./wpt/observable-constructor.any.js"></script>
24+
<script defer src="./wpt/observable-event-target.window.js"></script>
25+
<script defer src="./wpt/observable-find.any.js"></script>
26+
<script defer src="./wpt/observable-forEach.window.js"></script>
27+
<script defer src="./wpt/observable-map.any.js"></script>
28+
<script defer src="./wpt/observable-switchMap.any.js"></script>
29+
<script defer src="./wpt/observable-toArray.any.js"></script>
30+
<script defer src="./wpt/observable-constructor.window.js"></script>
31+
<script defer src="./wpt/observable-every.any.js"></script>
32+
<script defer src="./wpt/observable-first.any.js"></script>
33+
<script defer src="./wpt/observable-from.any.js"></script>
34+
<script defer src="./wpt/observable-map.window.js"></script>
35+
<script defer src="./wpt/observable-take.any.js"></script>
36+
</head>
37+
</html>

test/wpt/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
These tests were all copied from WPT https://wpt.fyi/results/dom/observable/tentative?label=experimental&label=master&aligned
2+
3+
Commit hash 39a3d27eb95f19313755d0098bc637ebbb96dfc5

test/wpt/idlharness.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!doctype html>
2+
<meta charset="utf-8" />
3+
<meta name="author" title="Keith Cirkel" href="mailto:keithamus@github.com" />
4+
<link rel="help" href="https://github.com/WICG/observable" />
5+
<script src="/resources/testharness.js"></script>
6+
<script src="/resources/testharnessreport.js"></script>
7+
<script src="/resources/WebIDLParser.js"></script>
8+
<script src="/resources/idlharness.js"></script>
9+
10+
<script>
11+
idl_test(["observable.tentative"], ["dom"], (idl_array) => {
12+
idl_array.add_objects({
13+
Observable: ["new Observable(() => {})"],
14+
Subscriber: [
15+
"(() => { let s = null; new Observable(_s => s = _s).subscribe({}); return s })()",
16+
],
17+
});
18+
});
19+
</script>
20+
</t>

test/wpt/observable-catch.any.js

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
test(() => {
2+
const source = new Observable(subscriber => {
3+
subscriber.next(1);
4+
subscriber.next(2);
5+
subscriber.next(3);
6+
subscriber.complete();
7+
});
8+
9+
const caughtObservable = source.catch(() => {
10+
assert_unreached("catch() is not called");
11+
});
12+
13+
const results = [];
14+
15+
caughtObservable.subscribe({
16+
next: value => results.push(value),
17+
complete: () => results.push('complete')
18+
});
19+
20+
assert_array_equals(results, [1, 2, 3, 'complete']);
21+
}, "catch(): Returns an Observable that is a pass-through for next()/complete()");
22+
23+
test(() => {
24+
let sourceError = new Error("from the source");
25+
const source = new Observable(subscriber => {
26+
subscriber.next(1);
27+
subscriber.next(2);
28+
subscriber.error(sourceError);
29+
});
30+
31+
const caughtObservable = source.catch(error => {
32+
assert_equals(error, sourceError);
33+
return new Observable(subscriber => {
34+
subscriber.next(3);
35+
subscriber.complete();
36+
});
37+
});
38+
39+
const results = [];
40+
41+
caughtObservable.subscribe({
42+
next: value => results.push(value),
43+
complete: () => results.push("complete"),
44+
});
45+
46+
assert_array_equals(results, [1, 2, 3, 'complete']);
47+
}, "catch(): Handle errors from source and flatten to a new Observable");
48+
49+
test(() => {
50+
const sourceError = new Error("from the source");
51+
const source = new Observable(subscriber => {
52+
subscriber.next(1);
53+
subscriber.next(2);
54+
subscriber.error(sourceError);
55+
});
56+
57+
const catchCallbackError = new Error("from the catch callback");
58+
const caughtObservable = source.catch(error => {
59+
assert_equals(error, sourceError);
60+
throw catchCallbackError;
61+
});
62+
63+
const results = [];
64+
65+
caughtObservable.subscribe({
66+
next: value => results.push(value),
67+
error: error => {
68+
results.push(error);
69+
},
70+
complete: () => results.push('complete'),
71+
});
72+
73+
assert_array_equals(results, [1, 2, catchCallbackError]);
74+
}, "catch(): Errors thrown in the catch() callback are sent to the consumer's error handler");
75+
76+
test(() => {
77+
// A common use case is logging and keeping the stream alive.
78+
const source = new Observable(subscriber => {
79+
subscriber.next(1);
80+
subscriber.next(2);
81+
subscriber.next(3);
82+
subscriber.complete();
83+
});
84+
85+
const flatteningError = new Error("from the flattening operation");
86+
function errorsOnTwo(value) {
87+
return new Observable(subscriber => {
88+
if (value === 2) {
89+
subscriber.error(flatteningError);
90+
} else {
91+
subscriber.next(value);
92+
subscriber.complete();
93+
}
94+
});
95+
}
96+
97+
const results = [];
98+
99+
source.flatMap(value => errorsOnTwo(value)
100+
.catch(error => {
101+
results.push(error);
102+
// This empty array converts to an Observable which automatically
103+
// completes.
104+
return [];
105+
})
106+
).subscribe({
107+
next: value => results.push(value),
108+
complete: () => results.push("complete")
109+
});
110+
111+
assert_array_equals(results, [1, flatteningError, 3, "complete"]);
112+
}, "catch(): CatchHandler can return an empty iterable");
113+
114+
promise_test(async () => {
115+
const sourceError = new Error("from the source");
116+
const source = new Observable(subscriber => {
117+
subscriber.next(1);
118+
subscriber.next(2);
119+
subscriber.error(sourceError);
120+
});
121+
122+
const caughtObservable = source.catch(error => {
123+
assert_equals(error, sourceError);
124+
return Promise.resolve(error.message);
125+
});
126+
127+
const results = await caughtObservable.toArray();
128+
129+
assert_array_equals(results, [1, 2, "from the source"]);
130+
}, "catch(): CatchHandler can return a Promise");
131+
132+
promise_test(async () => {
133+
const source = new Observable(subscriber => {
134+
subscriber.next(1);
135+
subscriber.next(2);
136+
subscriber.error(new Error('from the source'));
137+
});
138+
139+
const caughtObservable = source.catch(async function* (error) {
140+
assert_true(error instanceof Error);
141+
assert_equals(error.message, 'from the source');
142+
yield 3;
143+
});
144+
145+
const results = await caughtObservable.toArray();
146+
147+
assert_array_equals(results, [1, 2, 3], 'catch(): should handle returning an observable');
148+
}, 'catch(): should handle returning an async iterable');
149+
150+
test(() => {
151+
const sourceError = new Error("from the source");
152+
const source = new Observable(subscriber => {
153+
subscriber.next(1);
154+
subscriber.next(2);
155+
subscriber.error(sourceError);
156+
});
157+
158+
const caughtObservable = source.catch(error => {
159+
assert_equals(error, sourceError);
160+
// Primitive values like this are not convertible to an Observable, via the
161+
// `from()` semantics.
162+
return 3;
163+
});
164+
165+
const results = [];
166+
167+
caughtObservable.subscribe({
168+
next: value => results.push(value),
169+
error: error => {
170+
assert_true(error instanceof TypeError);
171+
results.push("TypeError");
172+
},
173+
complete: () => results.push("complete"),
174+
});
175+
176+
assert_array_equals(results, [1, 2, "TypeError"]);
177+
}, "catch(): CatchHandler emits an error if the value returned is not " +
178+
"convertible to an Observable");
179+
180+
test(() => {
181+
const source = new Observable(subscriber => {
182+
susbcriber.error(new Error("from the source"));
183+
});
184+
185+
const results = [];
186+
187+
const innerSubscriptionError = new Error("CatchHandler subscription error");
188+
const catchObservable = source.catch(() => {
189+
results.push('CatchHandler invoked');
190+
return new Observable(subscriber => {
191+
throw innerSubscriptionError;
192+
});
193+
});
194+
195+
catchObservable.subscribe({
196+
error: e => {
197+
results.push(e);
198+
}
199+
});
200+
201+
assert_array_equals(results, ['CatchHandler invoked', innerSubscriptionError]);
202+
}, "catch(): CatchHandler returns an Observable that throws immediately on " +
203+
"subscription");
204+
205+
// This test asserts that the relationship between (a) the AbortSignal passed
206+
// into `subscribe()` and (b) the AbortSignal associated with the Observable
207+
// returned from `catch()`'s CatchHandler is not a "dependent" relationship.
208+
// This is important because Observables have moved away from the "dependent
209+
// abort signal" infrastructure in https://github.com/WICG/observable/pull/154,
210+
// and this test asserts so.
211+
//
212+
// Here are all of the associated Observables and signals in this test:
213+
// 1. Raw outer signal passed into `subscribe()`
214+
// 2. catchObservable's inner Subscriber's signal
215+
// a. Per the above PR, and Subscriber's initialization logic [1], this
216+
// signal is set to abort in response to (1)'s abort algorithms. This
217+
// means its "abort" event gets fired before (1)'s.
218+
// 3. Inner CatchHandler-returned Observable's Subscriber's signal
219+
// a. Also per [1], this is set to abort in response to (2)'s abort
220+
// algorithms, since we subscribe to this "inner Observable" with (2)'s
221+
// signal as the `SubscribeOptions#signal`.
222+
//
223+
// (1), (2), and (3) above all form an abort chain:
224+
// (1) --> (2) --> (3)
225+
//
226+
// …such that when (1) aborts, its abort algorithms immediately abort (2),
227+
// whose abort algorithms immediately abort (3). Finally on the way back up the
228+
// chain, (3)'s `abort` event is fired, (2)'s `abort` event is fired, and then
229+
// (1)'s `abort` event is fired. This ordering of abort events is what this test
230+
// ensures.
231+
//
232+
// [1]: https://wicg.github.io/observable/#ref-for-abortsignal-add
233+
test(() => {
234+
const results = [];
235+
const source = new Observable(subscriber =>
236+
susbcriber.error(new Error("from the source")));
237+
238+
const catchObservable = source.catch(() => {
239+
return new Observable(subscriber => {
240+
subscriber.addTeardown(() => results.push('inner teardown'));
241+
subscriber.signal.addEventListener('abort',
242+
e => results.push('inner signal abort'));
243+
244+
// No values or completion. We'll just wait for the subscriber to abort
245+
// its subscription.
246+
});
247+
});
248+
249+
const ac = new AbortController();
250+
ac.signal.addEventListener('abort', e => results.push('outer signal abort'));
251+
catchObservable.subscribe({}, {signal: ac.signal});
252+
ac.abort();
253+
254+
assert_array_equals(results, ['inner signal abort', 'inner teardown', 'outer signal abort']);
255+
}, "catch(): Abort order between outer AbortSignal and inner CatchHandler subscriber's AbortSignal");

0 commit comments

Comments
 (0)