1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host;
16
17 import static java.nio.charset.StandardCharsets.UTF_16LE;
18 import static org.htmlunit.junit.annotation.TestedBrowser.FF;
19 import static org.htmlunit.junit.annotation.TestedBrowser.FF_ESR;
20
21 import java.io.IOException;
22 import java.nio.ByteBuffer;
23 import java.util.Set;
24 import java.util.concurrent.CopyOnWriteArraySet;
25
26 import org.eclipse.jetty.websocket.api.Session;
27 import org.eclipse.jetty.websocket.api.WebSocketAdapter;
28 import org.eclipse.jetty.websocket.server.WebSocketHandler;
29 import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
30 import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
31 import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
32 import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
33 import org.htmlunit.HttpHeader;
34 import org.htmlunit.WebDriverTestCase;
35 import org.htmlunit.junit.BrowserRunner;
36 import org.htmlunit.junit.annotation.Alerts;
37 import org.htmlunit.junit.annotation.HtmlUnitNYI;
38 import org.htmlunit.junit.annotation.NotYetImplemented;
39 import org.junit.After;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 import org.openqa.selenium.By;
43 import org.openqa.selenium.WebDriver;
44 import org.openqa.selenium.WebElement;
45
46
47
48
49
50
51
52
53 @RunWith(BrowserRunner.class)
54 public class WebSocketTest extends WebDriverTestCase {
55
56
57
58
59 @Test
60 @Alerts({"§§URL§§", "", "blob"})
61 public void initialNoServerAvailable() throws Exception {
62 final String html = DOCTYPE_HTML
63 + "<html>\n"
64 + "<head>\n"
65 + "<script>\n"
66 + LOG_TITLE_FUNCTION
67 + " function test() {\n"
68 + " var location = 'ws://localhost:" + PORT2 + "/';\n"
69 + " var ws = new WebSocket(location);\n"
70 + " log(ws.url);\n"
71 + " log(ws.protocol);\n"
72
73
74
75 + " log(ws.binaryType);\n"
76 + " }\n"
77 + "</script>\n"
78 + "</head>\n"
79 + "<body onload='test()'>\n"
80 + "</body></html>";
81
82 expandExpectedAlertsVariables("ws://localhost:" + PORT2 + "/");
83 final WebDriver driver = loadPage2(html);
84 verifyTitle2(DEFAULT_WAIT_TIME, driver, getExpectedAlerts());
85 }
86
87
88
89
90 @Test
91 @Alerts({"[object WebSocket]", "§§URL§§"})
92 public void earlyConstruction() throws Exception {
93 final String html = DOCTYPE_HTML
94 + "<html><head><script>\n"
95 + LOG_TITLE_FUNCTION
96 + " function test() {\n"
97 + " var location = 'ws://localhost:" + PORT + "/';\n"
98 + " var ws = new WebSocket(location);\n"
99 + " log(ws);\n"
100 + " log(ws.url);\n"
101 + " }\n"
102 + " test();\n"
103 + "</script>\n"
104 + "</head>\n"
105 + "<body>\n"
106 + "</body></html>";
107
108 expandExpectedAlertsVariables("ws://localhost:" + PORT + "/");
109 final WebDriver driver = loadPage2(html);
110 verifyTitle2(DEFAULT_WAIT_TIME, driver, getExpectedAlerts());
111 }
112
113
114
115
116 @Test
117 @Alerts(DEFAULT = {"exception no param", "ws://localhost:22222/undefined", "ws://localhost:22222/null",
118 "ws://localhost:22222/", "ws://localhost:22222/", "exception invalid"},
119 FF = {"exception no param", "ws://localhost:22222/undefined", "ws://localhost:22222/null",
120 "exception empty", "ws://localhost:22222/", "exception invalid"},
121 FF_ESR = {"exception no param", "ws://localhost:22222/undefined", "ws://localhost:22222/null",
122 "exception empty", "ws://localhost:22222/", "exception invalid"})
123 @HtmlUnitNYI(
124 CHROME = {"exception no param", "ws://localhost:22222/undefined", "ws://localhost:22222/null",
125 "ws://localhost:22222/", "ws://localhost:22222/", "ws://localhost:22222/#"},
126 EDGE = {"exception no param", "ws://localhost:22222/undefined", "ws://localhost:22222/null",
127 "ws://localhost:22222/", "ws://localhost:22222/", "ws://localhost:22222/#"},
128 FF = {"exception no param", "ws://localhost:22222/undefined", "ws://localhost:22222/null",
129 "ws://localhost:22222/", "ws://localhost:22222/", "ws://localhost:22222/#"},
130 FF_ESR = {"exception no param", "ws://localhost:22222/undefined", "ws://localhost:22222/null",
131 "ws://localhost:22222/", "ws://localhost:22222/", "ws://localhost:22222/#"})
132 public void initialWithoutUrl() throws Exception {
133 final String html = DOCTYPE_HTML
134 + "<html><head><script>\n"
135 + LOG_TITLE_FUNCTION
136 + " function test() {\n"
137 + " try {\n"
138 + " let ws = new WebSocket();\n"
139 + " log(ws.url);"
140 + " } catch(e) { log('exception no param') }\n"
141
142 + " try {\n"
143 + " let ws = new WebSocket(undefined);\n"
144 + " log(ws.url);"
145 + " } catch(e) { log('exception undefined') }\n"
146
147 + " try {\n"
148 + " let ws = new WebSocket(null);\n"
149 + " log(ws.url);"
150 + " } catch(e) { log('exception null') }\n"
151
152 + " try {\n"
153 + " let ws = new WebSocket('');\n"
154 + " log(ws.url);"
155 + " } catch(e) { log('exception empty') }\n"
156
157 + " try {\n"
158 + " let ws = new WebSocket(' ');\n"
159 + " log(ws.url);"
160 + " } catch(e) { log('exception blank') }\n"
161
162 + " try {\n"
163 + " let ws = new WebSocket('#');\n"
164 + " log(ws.url);"
165 + " } catch(e) { log('exception invalid') }\n"
166 + " }\n"
167 + "</script></head><body onload='test()'>\n"
168 + "</body></html>";
169
170 loadPageVerifyTitle2(html);
171 }
172
173
174
175
176 @Test
177 @Alerts({"blob", "blob", "arraybuffer", "blob", "blob"})
178 public void binaryType() throws Exception {
179 final String html = DOCTYPE_HTML
180 + "<html><head><script>\n"
181 + LOG_TITLE_FUNCTION
182 + " function test() {\n"
183 + " var location = 'ws://localhost:" + PORT + "/';\n"
184 + " var ws = new WebSocket(location);\n"
185 + " log(ws.binaryType);\n"
186
187 + " try {\n"
188 + " ws.binaryType = 'abc';\n"
189 + " log(ws.binaryType);\n"
190 + " } catch(e) { logEx(e) }\n"
191
192 + " try {\n"
193 + " ws.binaryType = 'arraybuffer';\n"
194 + " log(ws.binaryType);\n"
195 + " } catch(e) { logEx(e) }\n"
196
197 + " try {\n"
198 + " ws.binaryType = 'blob';\n"
199 + " log(ws.binaryType);\n"
200 + " } catch(e) { logEx(e) }\n"
201
202 + " try {\n"
203 + " ws.binaryType = '';\n"
204 + " log(ws.binaryType);\n"
205 + " } catch(e) { logEx(e) }\n"
206 + " }\n"
207 + "</script></head><body onload='test()'>\n"
208 + "</body></html>";
209
210 loadPageVerifyTitle2(html);
211 }
212
213
214
215
216
217 @Test
218 public void chat() throws Exception {
219 final String firstResponse = "Browser: has joined!";
220 final String secondResponse = "Browser: Hope you are fine!";
221
222 startWebServer("src/test/resources/org/htmlunit/javascript/host",
223 null, null, new ChatWebSocketHandler());
224 try {
225 final WebDriver driver = getWebDriver();
226 driver.get(URL_FIRST + "WebSocketTest_chat.html");
227
228 driver.findElement(By.id("username")).sendKeys("Browser");
229 driver.findElement(By.id("joinB")).click();
230
231 assertVisible("joined", driver);
232
233 final WebElement chatE = driver.findElement(By.id("chat"));
234 long maxWait = System.currentTimeMillis() + DEFAULT_WAIT_TIME.toMillis();
235
236 do {
237 Thread.sleep(100);
238 }
239 while (chatE.getText().length() <= firstResponse.length() && System.currentTimeMillis() < maxWait);
240
241 assertEquals(firstResponse, chatE.getText());
242
243 driver.findElement(By.id("phrase")).sendKeys("Hope you are fine!");
244 driver.findElement(By.id("sendB")).click();
245
246 maxWait = System.currentTimeMillis() + DEFAULT_WAIT_TIME.toMillis();
247 do {
248 Thread.sleep(100);
249 }
250 while (!chatE.getText().contains(secondResponse) && System.currentTimeMillis() < maxWait);
251
252 assertEquals(firstResponse + "\n" + secondResponse, chatE.getText());
253 }
254 finally {
255 stopWebServers();
256 }
257 }
258
259 private static class ChatWebSocketHandler extends WebSocketHandler {
260
261 private final Set<ChatWebSocket> webSockets_ = new CopyOnWriteArraySet<>();
262
263 ChatWebSocketHandler() {
264 }
265
266 @Override
267 public void configure(final WebSocketServletFactory factory) {
268 factory.register(ChatWebSocket.class);
269 factory.setCreator(new WebSocketCreator() {
270 @Override
271 public Object createWebSocket(final ServletUpgradeRequest servletUpgradeRequest,
272 final ServletUpgradeResponse servletUpgradeResponse) {
273 return new ChatWebSocket();
274 }
275 });
276 }
277
278 private class ChatWebSocket extends WebSocketAdapter {
279 private Session session_;
280
281 ChatWebSocket() {
282 }
283
284 @Override
285 public void onWebSocketConnect(final Session session) {
286 session_ = session;
287 webSockets_.add(this);
288 }
289
290 @Override
291 public void onWebSocketText(final String data) {
292 try {
293 for (final ChatWebSocket webSocket : webSockets_) {
294 webSocket.session_.getRemote().sendString(data);
295 }
296 }
297 catch (final IOException e) {
298 session_.close();
299 }
300 }
301
302 @Override
303 public void onWebSocketClose(final int closeCode, final String message) {
304 webSockets_.remove(this);
305 }
306 }
307 }
308
309
310
311
312 @After
313 @Override
314 public void releaseResources() {
315 super.releaseResources();
316
317 for (final Thread thread : Thread.getAllStackTraces().keySet()) {
318 if (thread.getName().contains("WebSocket")) {
319 try {
320
321
322 Thread.sleep(400);
323 }
324 catch (final InterruptedException e) {
325 e.printStackTrace();
326 }
327 }
328 }
329
330 String lastFailing = null;
331 for (final java.util.Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
332 final Thread thread = entry.getKey();
333 if (thread.getName().contains("WebSocket")) {
334 lastFailing = thread.getName();
335 System.err.println();
336 System.err.println("WebSocket thread named '" + lastFailing + "' still running");
337 final StackTraceElement[] traces = entry.getValue();
338 for (int i = 0; i < traces.length; i++) {
339 System.err.println(traces[i]);
340 }
341 }
342 }
343
344 assertNull("WebSocket thread named '" + lastFailing + "' still running", lastFailing);
345 }
346
347
348
349
350 @Test
351 @Alerts({": myname=My value!1", ": myname=My value!2"})
352 public void cookies() throws Exception {
353 final String[] expected = getExpectedAlerts();
354
355 startWebServer("src/test/resources/org/htmlunit/javascript/host",
356 null, null, new CookiesWebSocketHandler());
357 try {
358 final WebDriver driver = getWebDriver();
359 driver.get(URL_FIRST + "WebSocketTest_cookies.html");
360
361 driver.findElement(By.id("username")).sendKeys("Browser");
362 driver.findElement(By.id("joinB")).click();
363 final WebElement chatE = driver.findElement(By.id("chat"));
364
365 long maxWait = System.currentTimeMillis() + DEFAULT_WAIT_TIME.toMillis();
366 do {
367 Thread.sleep(100);
368 }
369 while (chatE.getText().length() <= expected[0].length() && System.currentTimeMillis() < maxWait);
370
371 assertEquals(expected[0], chatE.getText());
372
373 driver.findElement(By.id("phrase")).sendKeys("Hope you are fine!");
374 driver.findElement(By.id("sendB")).click();
375
376 maxWait = System.currentTimeMillis() + DEFAULT_WAIT_TIME.toMillis();
377 do {
378 Thread.sleep(100);
379 }
380 while (!chatE.getText().contains(expected[1]) && System.currentTimeMillis() < maxWait);
381
382 assertEquals(expected[0] + "\n" + expected[1], chatE.getText());
383 }
384 finally {
385 stopWebServers();
386 }
387 }
388
389 private static class CookiesWebSocketHandler extends WebSocketHandler {
390
391 private final Set<CookiesWebSocket> webSockets_ = new CopyOnWriteArraySet<>();
392
393 CookiesWebSocketHandler() {
394 }
395
396 @Override
397 public void configure(final WebSocketServletFactory factory) {
398 factory.register(CookiesWebSocket.class);
399 factory.setCreator(new WebSocketCreator() {
400 @Override
401 public Object createWebSocket(final ServletUpgradeRequest servletUpgradeRequest,
402 final ServletUpgradeResponse servletUpgradeResponse) {
403 return new CookiesWebSocket();
404 }
405 });
406 }
407
408 private class CookiesWebSocket extends WebSocketAdapter {
409 private Session session_;
410 private int counter_ = 1;
411
412 CookiesWebSocket() {
413 }
414
415 @Override
416 public void onWebSocketConnect(final Session session) {
417 session_ = session;
418 webSockets_.add(this);
419 }
420
421 @Override
422 public void onWebSocketText(final String data) {
423 try {
424 final String cookie = session_.getUpgradeRequest().getHeaders()
425 .get(HttpHeader.COOKIE).get(0) + counter_++;
426 for (final CookiesWebSocket webSocket : webSockets_) {
427 webSocket.session_.getRemote().sendString(cookie);
428 }
429 }
430 catch (final IOException e) {
431 session_.close();
432 }
433 }
434
435 @Override
436 public void onWebSocketClose(final int closeCode, final String message) {
437 webSockets_.remove(this);
438 }
439 }
440 }
441
442
443
444
445 @Test
446 @Alerts({"onOpenListener",
447 "onOpen", "open", "[object WebSocket]", "[object WebSocket]",
448 "undefined", "undefined", "undefined", "undefined",
449 "onMessageTextListener", "message", "[object WebSocket]", "[object WebSocket]",
450 "server_text", "§§URL§§", "", "null",
451 "onMessageText", "message", "[object WebSocket]", "[object WebSocket]",
452 "server_text", "§§URL§§", "", "null",
453 "onMessageBinaryListener", "message", "[object WebSocket]", "[object WebSocket]",
454 "[object ArrayBuffer]", "§§URL§§", "", "null",
455 "onMessageBinary", "message", "[object WebSocket]", "[object WebSocket]",
456 "[object ArrayBuffer]", "§§URL§§", "", "null",
457 "onCloseListener code: 1000",
458 "onClose code: 1000"})
459 public void events() throws Exception {
460 expandExpectedAlertsVariables("ws://localhost:" + PORT);
461 final String expected = String.join("\n", getExpectedAlerts());
462
463 startWebServer("src/test/resources/org/htmlunit/javascript/host",
464 null, null, new EventsWebSocketHandler());
465 try {
466 final WebDriver driver = getWebDriver();
467 driver.get(URL_FIRST + "WebSocketTest_events.html");
468
469 final WebElement logElement = driver.findElement(By.id("log"));
470 final long maxWait = System.currentTimeMillis() + DEFAULT_WAIT_TIME.toMillis();
471
472 String text;
473 do {
474 Thread.sleep(100);
475
476 text = logElement.getDomProperty("value").trim().replaceAll("\r", "");
477 }
478 while (text.length() <= expected.length() && System.currentTimeMillis() < maxWait);
479
480 assertEquals(expected, text);
481 }
482 finally {
483 stopWebServers();
484 }
485 }
486
487
488
489
490 @Test
491 @Alerts(DEFAULT = {"onOpenListener",
492 "onOpen", "open", "[object WebSocket]", "[object WebSocket]",
493 "undefined", "undefined", "undefined", "undefined",
494 "onMessageTextListener", "message", "[object WebSocket]", "[object WebSocket]",
495 "server_text", "§§URL§§", "", "null",
496 "onMessageText", "message", "[object WebSocket]", "[object WebSocket]",
497 "server_text", "§§URL§§", "", "null",
498 "onMessageBinaryListener", "message", "[object WebSocket]", "[object WebSocket]",
499 "[object ArrayBuffer]", "§§URL§§", "", "null",
500 "onMessageBinary", "message", "[object WebSocket]", "[object WebSocket]",
501 "[object ArrayBuffer]", "§§URL§§", "", "null",
502 "onCloseListener code: 1000 wasClean: true",
503 "onClose code: 1000 wasClean: true"},
504 FF = {"onOpenListener",
505 "onOpen", "open", "[object WebSocket]", "[object WebSocket]",
506 "undefined", "undefined", "undefined", "undefined",
507 "onMessageTextListener", "message", "[object WebSocket]", "[object WebSocket]",
508 "server_text", "§§URL§§", "", "null",
509 "onMessageText", "message", "[object WebSocket]", "[object WebSocket]",
510 "server_text", "§§URL§§", "", "null",
511 "onMessageBinaryListener", "message", "[object WebSocket]", "[object WebSocket]",
512 "[object ArrayBuffer]", "§§URL§§", "", "null",
513 "onMessageBinary", "message", "[object WebSocket]", "[object WebSocket]",
514 "[object ArrayBuffer]", "§§URL§§", "", "null",
515 "onCloseListener code: 1000 wasClean: false",
516 "onClose code: 1000 wasClean: false"},
517 FF_ESR = {"onOpenListener",
518 "onOpen", "open", "[object WebSocket]", "[object WebSocket]",
519 "undefined", "undefined", "undefined", "undefined",
520 "onMessageTextListener", "message", "[object WebSocket]", "[object WebSocket]",
521 "server_text", "§§URL§§", "", "null",
522 "onMessageText", "message", "[object WebSocket]", "[object WebSocket]",
523 "server_text", "§§URL§§", "", "null",
524 "onMessageBinaryListener", "message", "[object WebSocket]", "[object WebSocket]",
525 "[object ArrayBuffer]", "§§URL§§", "", "null",
526 "onMessageBinary", "message", "[object WebSocket]", "[object WebSocket]",
527 "[object ArrayBuffer]", "§§URL§§", "", "null",
528 "onCloseListener code: 1000 wasClean: false",
529 "onClose code: 1000 wasClean: false"})
530 @NotYetImplemented({FF, FF_ESR})
531 public void wasClean() throws Exception {
532 expandExpectedAlertsVariables("ws://localhost:" + PORT);
533 final String expected = String.join("\n", getExpectedAlerts());
534
535 startWebServer("src/test/resources/org/htmlunit/javascript/host",
536 null, null, new EventsWebSocketHandler());
537 try {
538 final WebDriver driver = getWebDriver();
539 driver.get(URL_FIRST + "WebSocketTest_wasClean.html");
540
541 final WebElement logElement = driver.findElement(By.id("log"));
542 final long maxWait = System.currentTimeMillis() + DEFAULT_WAIT_TIME.toMillis();
543
544 String text;
545 do {
546 Thread.sleep(100);
547
548 text = logElement.getDomProperty("value").trim().replaceAll("\r", "");
549 }
550 while (text.length() <= expected.length() && System.currentTimeMillis() < maxWait);
551
552 assertEquals(expected, text);
553 }
554 finally {
555 stopWebServers();
556 }
557 }
558
559
560
561
562 @Test
563 @Alerts({"onError[object Event]",
564 "onCloseListener code: 1006 wasClean: false",
565 "onClose code: 1006 wasClean: false"})
566 public void eventsNoSocketServer() throws Exception {
567 startWebServer("src/test/resources/org/htmlunit/javascript/host", null, null, null);
568 try {
569 final WebDriver driver = getWebDriver();
570 driver.get(URL_FIRST + "WebSocketTest_wasClean.html");
571
572 final WebElement logElement = driver.findElement(By.id("log"));
573 int counter = 0;
574 String text;
575 do {
576 Thread.sleep(DEFAULT_WAIT_TIME.toMillis());
577
578 text = logElement.getDomProperty("value").trim().replaceAll("\r", "");
579 }
580 while (text.length() > 0 && counter++ < 10);
581
582 assertEquals(String.join("\n", getExpectedAlerts()), text);
583 }
584 finally {
585 stopWebServers();
586 }
587 }
588
589 private static void assertVisible(final String domId, final WebDriver driver) throws Exception {
590 final WebElement domE = driver.findElement(By.id(domId));
591 int counter = 0;
592 do {
593 Thread.sleep(100);
594 }
595 while (!domE.isDisplayed() && counter++ < 10);
596
597 assertEquals("Node should be visible, domId: " + domId, true, domE.isDisplayed());
598 }
599
600 private static class EventsWebSocketHandler extends WebSocketHandler {
601
602 EventsWebSocketHandler() {
603 }
604
605 @Override
606 public void configure(final WebSocketServletFactory factory) {
607 factory.register(EventsWebSocket.class);
608 factory.setCreator(new WebSocketCreator() {
609 @Override
610 public EventsWebSocket createWebSocket(final ServletUpgradeRequest servletUpgradeRequest,
611 final ServletUpgradeResponse servletUpgradeResponse) {
612 return new EventsWebSocket();
613 }
614 });
615 }
616
617 private static class EventsWebSocket extends WebSocketAdapter {
618
619 EventsWebSocket() {
620 }
621
622 @Override
623 public void onWebSocketText(final String data) {
624 if ("text".equals(data)) {
625 try {
626 getRemote().sendString("server_text");
627 }
628 catch (final IOException e) {
629 throw new IllegalStateException(e.getMessage(), e);
630 }
631 }
632 else if ("close".equals(data)) {
633 getSession().close();
634 }
635 else {
636 throw new IllegalArgumentException("Unknown request: " + data);
637 }
638 }
639
640 @Override
641 public void onWebSocketBinary(final byte[] payload, final int offset, final int len) {
642 final String data = new String(payload, offset, len, UTF_16LE);
643 if ("binary".equals(data)) {
644 final ByteBuffer response = ByteBuffer.wrap("server_binary".getBytes(UTF_16LE));
645 try {
646 getRemote().sendBytes(response);
647 }
648 catch (final IOException e) {
649 throw new IllegalStateException(e.getMessage(), e);
650 }
651 }
652 else {
653 throw new IllegalArgumentException("Unknown request: " + data);
654 }
655 }
656 }
657 }
658
659
660
661
662 @Test
663 @Alerts("true")
664 public void prototypeUrl() throws Exception {
665 final String html = DOCTYPE_HTML
666 + "<html><head><script>\n"
667 + LOG_TITLE_FUNCTION
668 + " function test() {\n"
669 + " try {\n"
670 + " var u = WebSocket.prototype.url;\n"
671 + " log(u);\n"
672 + " } catch(e) { log(e instanceof TypeError) }\n"
673 + " }\n"
674 + "</script></head><body onload='test()'>\n"
675 + "</body></html>";
676
677 loadPageVerifyTitle2(html);
678 }
679
680
681
682
683 @Test
684 public void socketsGetClosedOnPageReplace() throws Exception {
685 startWebServer("src/test/resources/org/htmlunit/javascript/host",
686 null, null, new ChatWebSocketHandler());
687 try {
688 final WebDriver driver = getWebDriver();
689 driver.get(URL_FIRST + "WebSocketTest_chat.html");
690
691 driver.findElement(By.id("username")).sendKeys("Browser");
692 driver.findElement(By.id("joinB")).click();
693
694 assertVisible("joined", driver);
695
696 driver.get(URL_FIRST + "plain.html");
697 }
698 finally {
699 stopWebServers();
700 }
701 }
702 }