diff --git a/conn.go b/conn.go index 8b9486c..f0e268b 100644 --- a/conn.go +++ b/conn.go @@ -10,14 +10,15 @@ import ( "io" "net" "net/url" + "sync/atomic" "time" - "github.com/gopherjs/gopherjs/js" + "github.com/gopherjs/gopherwasm/js" "github.com/gopherjs/websocket/websocketjs" ) -func beginHandlerOpen(ch chan error, removeHandlers func()) func(ev *js.Object) { - return func(ev *js.Object) { +func beginHandlerOpen(ch chan error, removeHandlers func()) func(ev js.Value) { + return func(ev js.Value) { removeHandlers() close(ch) } @@ -25,29 +26,24 @@ func beginHandlerOpen(ch chan error, removeHandlers func()) func(ev *js.Object) // closeError allows a CloseEvent to be used as an error. type closeError struct { - *js.Object - Code int `js:"code"` - Reason string `js:"reason"` - WasClean bool `js:"wasClean"` + js.Value } func (e *closeError) Error() string { var cleanStmt string - if e.WasClean { + if e.Get("wasClean").Bool() { cleanStmt = "clean" } else { cleanStmt = "unclean" } - return fmt.Sprintf("CloseEvent: (%s) (%d) %s", cleanStmt, e.Code, e.Reason) + return fmt.Sprintf("CloseEvent: (%s) (%d) %s", cleanStmt, e.Get("code").Int(), e.Get("reason").String()) } -func beginHandlerClose(ch chan error, removeHandlers func()) func(ev *js.Object) { - return func(ev *js.Object) { +func beginHandlerClose(ch chan error, removeHandlers func()) func(ev js.Value) { + return func(ev js.Value) { removeHandlers() - go func() { - ch <- &closeError{Object: ev} - close(ch) - }() + ch <- &closeError{Value: ev} + close(ch) } } @@ -71,32 +67,35 @@ func Dial(url string) (net.Conn, error) { } conn := &conn{ WebSocket: ws, - ch: make(chan *messageEvent, 1), } + conn.isClosed = new(uint32) + *conn.isClosed = 0 conn.initialize() openCh := make(chan error, 1) var ( - openHandler func(ev *js.Object) - closeHandler func(ev *js.Object) + openHandler js.Callback + closeHandler js.Callback ) // Handlers need to be removed to prevent a panic when the WebSocket closes // immediately and fires both open and close before they can be removed. // This way, handlers are removed before the channel is closed. removeHandlers := func() { - ws.RemoveEventListener("open", false, openHandler) - ws.RemoveEventListener("close", false, closeHandler) + ws.RemoveEventListener("open", openHandler) + ws.RemoveEventListener("close", closeHandler) + openHandler.Release() + closeHandler.Release() } // We have to use variables for the functions so that we can remove the // event handlers afterwards. - openHandler = beginHandlerOpen(openCh, removeHandlers) - closeHandler = beginHandlerClose(openCh, removeHandlers) + openHandler = js.NewEventCallback(0, beginHandlerOpen(openCh, removeHandlers)) + closeHandler = js.NewEventCallback(0, beginHandlerClose(openCh, removeHandlers)) - ws.AddEventListener("open", false, openHandler) - ws.AddEventListener("close", false, closeHandler) + ws.AddEventListener("open", openHandler) + ws.AddEventListener("close", closeHandler) err, ok := <-openCh if ok && err != nil { @@ -110,41 +109,102 @@ func Dial(url string) (net.Conn, error) { type conn struct { *websocketjs.WebSocket - ch chan *messageEvent + isClosed *uint32 + + writeCh chan<- *messageEvent + readCh <-chan *messageEvent readBuf *bytes.Reader + onMessageCallback js.Callback + onCloseCallback js.Callback + readDeadline time.Time } type messageEvent struct { - *js.Object - Data *js.Object `js:"data"` + js.Value + // Data js.Value `js:"data"` } -func (c *conn) onMessage(event *js.Object) { - go func() { - c.ch <- &messageEvent{Object: event} - }() +func (c *conn) onMessage(event js.Value) { + c.writeCh <- &messageEvent{Value: event} } -func (c *conn) onClose(event *js.Object) { - go func() { - // We queue nil to the end so that any messages received prior to - // closing get handled first. - c.ch <- nil - }() +func (c *conn) onClose(event js.Value) { + // We queue nil to the end so that any messages received prior to + // closing get handled first. + swapped := atomic.CompareAndSwapUint32(c.isClosed, 0, 1) + if swapped { + close(c.writeCh) + c.RemoveEventListener("message", c.onMessageCallback) + c.onMessageCallback.Release() + c.RemoveEventListener("close", c.onCloseCallback) + c.onCloseCallback.Release() + } +} + +func (c *conn) Close() error { + err := c.WebSocket.Close() + c.onClose(js.Null()) + return err } // initialize adds all of the event handlers necessary for a conn to function. // It should never be called more than once and is already called if Dial was // used to create the conn. func (c *conn) initialize() { + writeChan := make(chan *messageEvent) + readChan := make(chan *messageEvent) + + c.writeCh = writeChan + c.readCh = readChan + + go c.bufferMessageEvents(writeChan, readChan) + // We need this so that received binary data is in ArrayBufferView format so // that it can easily be read. - c.BinaryType = "arraybuffer" + c.Set("binaryType", "arraybuffer") - c.AddEventListener("message", false, c.onMessage) - c.AddEventListener("close", false, c.onClose) + c.onMessageCallback = js.NewEventCallback(0, c.onMessage) + c.onCloseCallback = js.NewEventCallback(0, c.onClose) + + c.AddEventListener("message", c.onMessageCallback) + c.AddEventListener("close", c.onCloseCallback) +} + +func (c *conn) bufferMessageEvents(write chan *messageEvent, read chan *messageEvent) { + queue := make([]*messageEvent, 0, 16) + + getReadChan := func() chan *messageEvent { + if len(queue) == 0 { + return nil + } + + return read + } + + getQueuedEvent := func() *messageEvent { + if len(queue) == 0 { + return nil + } + + return queue[0] + } + + for len(queue) > 0 || write != nil { + select { + case newEvent, ok := <-write: + if !ok { + write = nil + } else { + queue = append(queue, newEvent) + } + case getReadChan() <- getQueuedEvent(): + queue = queue[1:] + } + } + + close(read) } // handleFrame handles a single frame received from the channel. This is a @@ -152,10 +212,6 @@ func (c *conn) initialize() { func (c *conn) handleFrame(message *messageEvent, ok bool) (*messageEvent, error) { if !ok { // The channel has been closed return nil, io.EOF - } else if message == nil { - // See onClose for the explanation about sending a nil item. - close(c.ch) - return nil, io.EOF } return message, nil @@ -170,7 +226,7 @@ func (c *conn) receiveFrame(observeDeadline bool) (*messageEvent, error) { now := time.Now() if now.After(c.readDeadline) { select { - case item, ok := <-c.ch: + case item, ok := <-c.readCh: return c.handleFrame(item, ok) default: return nil, errDeadlineReached @@ -184,18 +240,22 @@ func (c *conn) receiveFrame(observeDeadline bool) (*messageEvent, error) { } select { - case item, ok := <-c.ch: + case item, ok := <-c.readCh: return c.handleFrame(item, ok) case <-deadlineChan: return nil, errDeadlineReached } } -func getFrameData(obj *js.Object) []byte { +func getFrameData(obj js.Value) []byte { // Check if it's an array buffer. If so, convert it to a Go byte slice. - if constructor := obj.Get("constructor"); constructor == js.Global.Get("ArrayBuffer") { - uint8Array := js.Global.Get("Uint8Array").New(obj) - return uint8Array.Interface().([]byte) + if obj.InstanceOf(js.Global().Get("ArrayBuffer")) { + uint8Array := js.Global().Get("Uint8Array").New(obj) + data := make([]byte, uint8Array.Length()) + for i, arrayLen := 0, uint8Array.Length(); i < arrayLen; i++ { + data[i] = byte(uint8Array.Index(i).Int()) + } + return data } return []byte(obj.String()) } @@ -221,7 +281,7 @@ func (c *conn) Read(b []byte) (n int, err error) { return 0, err } - receivedBytes := getFrameData(frame.Data) + receivedBytes := getFrameData(frame.Get("data")) n = copy(b, receivedBytes) // Fast path: The entire frame's contents have been copied into b. @@ -237,7 +297,9 @@ func (c *conn) Read(b []byte) (n int, err error) { func (c *conn) Write(b []byte) (n int, err error) { // []byte is converted to an Uint8Array by GopherJS, which fullfils the // ArrayBufferView definition. - err = c.Send(b) + byteArray := js.TypedArrayOf(b) + defer byteArray.Release() + err = c.Send(byteArray.Value) if err != nil { return 0, err } @@ -258,7 +320,7 @@ func (c *conn) LocalAddr() net.Addr { // RemoteAddr returns the remote network address, based on // websocket.WebSocket.URL. func (c *conn) RemoteAddr() net.Addr { - wsURL, err := url.Parse(c.URL) + wsURL, err := url.Parse(c.Get("url").String()) if err != nil { // TODO(nightexcessive): Should we be panicking for this? panic(err) diff --git a/test/HOW_TO_TEST.md b/test/HOW_TO_TEST.md new file mode 100644 index 0000000..9d8c220 --- /dev/null +++ b/test/HOW_TO_TEST.md @@ -0,0 +1,5 @@ +To test: +* Run server.go +* Change to the inner test directory +* Run `go test -c -o main.wasm` +* Open `http://localhost:3000` diff --git a/test/server.go b/test/server.go index 1b2643c..75b9028 100644 --- a/test/server.go +++ b/test/server.go @@ -1,6 +1,8 @@ package main import ( + "crypto/rand" + "io" "net/http" "time" @@ -26,8 +28,73 @@ func main() { } })) + http.Handle("/ws/multiframe-static", websocket.Handler(func(ws *websocket.Conn) { + err := websocket.Message.Send(ws, []byte{0x00, 0x01, 0x02}) + if err != nil { + panic(err) + } + time.Sleep(500 * time.Millisecond) + err = websocket.Message.Send(ws, []byte{0x03, 0x04}) + if err != nil { + panic(err) + } + })) + + http.Handle("/ws/random-1mb", websocket.Handler(func(ws *websocket.Conn) { + for i := 0; i < 4; i++ { + data := make([]byte, 256*1024) + n, err := io.ReadAtLeast(rand.Reader, data, len(data)) + if err != nil { + panic(err) + } + + data = data[:n] + + err = websocket.Message.Send(ws, data) + if err != nil { + panic(err) + } + } + })) + http.Handle("/ws/wait-30s", websocket.Handler(func(ws *websocket.Conn) { - <-time.After(30 * time.Second) + eofChan := make(chan struct{}) + timeoutChan := time.After(30 * time.Second) + + go func() { + buf := make([]byte, 2) + for n, err := ws.Read(buf); ; { + if err == io.EOF || n == 0 { // for some reason, the websocket package returns 0 byte reads instead of an io.EOF error + eofChan <- struct{}{} + return + } else if err != nil { + panic(err) + } + } + }() + + select { + case <-eofChan: + case <-timeoutChan: + } + })) + + http.Handle("/ws/echo", websocket.Handler(func(ws *websocket.Conn) { + var toBeEchoed []byte + for { + toBeEchoed = toBeEchoed[:0] + err := websocket.Message.Receive(ws, &toBeEchoed) + if err == io.EOF { + break + } else if err != nil { + panic(err) + } + + err = websocket.Message.Send(ws, toBeEchoed) + if err != nil { + panic(err) + } + } })) err := http.ListenAndServe(":3000", nil) diff --git a/test/test/.gitignore b/test/test/.gitignore index cd1312e..66d129f 100644 --- a/test/test/.gitignore +++ b/test/test/.gitignore @@ -1,3 +1,2 @@ # Ignore compiled code -index.js -index.js.map +main.wasm diff --git a/test/test/index.go b/test/test/index.go deleted file mode 100644 index 7d2c6b4..0000000 --- a/test/test/index.go +++ /dev/null @@ -1,196 +0,0 @@ -//go:generate gopherjs build -m index.go - -package main - -import ( - "bytes" - "fmt" - "io" - "time" - - "github.com/gopherjs/gopherjs/js" - "github.com/gopherjs/websocket" - "github.com/gopherjs/websocket/websocketjs" - "github.com/rusco/qunit" -) - -func getWSBaseURL() string { - document := js.Global.Get("window").Get("document") - location := document.Get("location") - - wsProtocol := "ws" - if location.Get("protocol").String() == "https:" { - wsProtocol = "wss" - } - - return fmt.Sprintf("%s://%s:%s/ws/", wsProtocol, location.Get("hostname"), location.Get("port")) -} - -func main() { - wsBaseURL := getWSBaseURL() - - qunit.Module("websocketjs.WebSocket") - qunit.Test("Invalid URL", func(assert qunit.QUnitAssert) { - qunit.Expect(1) - - ws, err := websocketjs.New("blah://blah.example/invalid") - if err == nil { - ws.Close() - assert.Ok(false, "Got no error, but expected an invalid URL error") - return - } - - assert.Ok(true, fmt.Sprintf("Received an error: %s", err)) - }) - qunit.AsyncTest("Immediate close", func() interface{} { - qunit.Expect(2) - - ws, err := websocketjs.New(wsBaseURL + "immediate-close") - if err != nil { - qunit.Ok(false, fmt.Sprintf("Error opening WebSocket: %s", err)) - qunit.Start() - return nil - } - - ws.AddEventListener("open", false, func(ev *js.Object) { - qunit.Ok(true, "WebSocket opened") - }) - - ws.AddEventListener("close", false, func(ev *js.Object) { - const ( - CloseNormalClosure = 1000 - CloseNoStatusReceived = 1005 // IE10 hates it when the server closes without sending a close reason - ) - - closeEventCode := ev.Get("code").Int() - - if closeEventCode != CloseNormalClosure && closeEventCode != CloseNoStatusReceived { - qunit.Ok(false, fmt.Sprintf("WebSocket close was not clean (code %d)", closeEventCode)) - qunit.Start() - return - } - qunit.Ok(true, "WebSocket closed") - qunit.Start() - }) - - return nil - }) - - qunit.Module("websocket.Conn") - qunit.AsyncTest("Immediate close", func() interface{} { - go func() { - defer qunit.Start() - - ws, err := websocket.Dial(wsBaseURL + "immediate-close") - if err != nil { - qunit.Ok(false, fmt.Sprintf("Error opening WebSocket: %s", err)) - return - } - - qunit.Ok(true, "WebSocket opened") - - _, err = ws.Read(nil) - if err == io.EOF { - qunit.Ok(true, "Received EOF") - } else if err != nil { - qunit.Ok(false, fmt.Sprintf("Unexpected error in second read: %s", err)) - } else { - qunit.Ok(false, "Expected EOF in second read, got no error") - } - }() - - return nil - }) - qunit.AsyncTest("Failed open", func() interface{} { - go func() { - defer qunit.Start() - - ws, err := websocket.Dial(wsBaseURL + "404-not-found") - if err == nil { - ws.Close() - qunit.Ok(false, "Got no error, but expected an error in opening the WebSocket.") - return - } - - qunit.Ok(true, fmt.Sprintf("WebSocket failed to open: %s", err)) - }() - - return nil - }) - qunit.AsyncTest("Binary read", func() interface{} { - qunit.Expect(3) - - go func() { - defer qunit.Start() - - ws, err := websocket.Dial(wsBaseURL + "binary-static") - if err != nil { - qunit.Ok(false, fmt.Sprintf("Error opening WebSocket: %s", err)) - return - } - - qunit.Ok(true, "WebSocket opened") - - var expectedData = []byte{0x00, 0x01, 0x02, 0x03, 0x04} - - receivedData := make([]byte, len(expectedData)) - n, err := ws.Read(receivedData) - if err != nil { - qunit.Ok(false, fmt.Sprintf("Error in first read: %s", err)) - return - } - receivedData = receivedData[:n] - - if !bytes.Equal(receivedData, expectedData) { - qunit.Ok(false, fmt.Sprintf("Received data did not match expected data. Got % x, expected % x.", receivedData, expectedData)) - } else { - qunit.Ok(true, fmt.Sprintf("Received data: % x", receivedData)) - } - - _, err = ws.Read(receivedData) - if err == io.EOF { - qunit.Ok(true, "Received EOF") - } else if err != nil { - qunit.Ok(false, fmt.Sprintf("Unexpected error in second read: %s", err)) - } else { - qunit.Ok(false, "Expected EOF in second read, got no error") - } - }() - - return nil - }) - qunit.AsyncTest("Timeout", func() interface{} { - qunit.Expect(2) - - go func() { - defer qunit.Start() - - ws, err := websocket.Dial(wsBaseURL + "wait-30s") - if err != nil { - qunit.Ok(false, fmt.Sprintf("Error opening WebSocket: %s", err)) - return - } - - qunit.Ok(true, "WebSocket opened") - - start := time.Now() - ws.SetReadDeadline(start.Add(1 * time.Second)) - - _, err = ws.Read(nil) - if err != nil && err.Error() == "i/o timeout: deadline reached" { - totalTime := time.Now().Sub(start) - if totalTime < 750*time.Millisecond { - qunit.Ok(false, fmt.Sprintf("Timeout was too short: Received timeout after %s", totalTime)) - return - } - qunit.Ok(true, fmt.Sprintf("Received timeout after %s", totalTime)) - } else if err != nil { - qunit.Ok(false, fmt.Sprintf("Unexpected error in read: %s", err)) - } else { - qunit.Ok(false, "Expected timeout in read, got no error") - } - }() - - return nil - }) -} diff --git a/test/test/index.html b/test/test/index.html index d2a6570..5c8069b 100644 --- a/test/test/index.html +++ b/test/test/index.html @@ -2,13 +2,15 @@ - GopherJS - WebSocket tests - + WebSocket tests -
-
- - + + diff --git a/test/test/init_test.go b/test/test/init_test.go new file mode 100644 index 0000000..e67f704 --- /dev/null +++ b/test/test/init_test.go @@ -0,0 +1,65 @@ +package websocket_test + +import ( + "flag" + "fmt" + "syscall/js" + + "github.com/LinearZoetrope/testevents" +) + +func testStarted(ev testevents.Event) { + document := js.Global().Get("document") + body := document.Get("body") + + outsideElement := document.Call("createElement", "div") + outsideElement.Set("id", ev.Name) + + nameElement := document.Call("createElement", "span") + nameElement.Set("textContent", fmt.Sprintf("%s:", ev.Name)) + outsideElement.Call("appendChild", nameElement) + + nbspElement := document.Call("createElement", "span") + nbspElement.Set("innerHTML", " ") + outsideElement.Call("appendChild", nbspElement) + + body.Call("appendChild", outsideElement) +} + +func testPassed(ev testevents.Event) { + document := js.Global().Get("document") + outsideElement := document.Call("getElementById", ev.Name) + + statusElement := document.Call("createElement", "span") + statusElement.Set("textContent", "PASSED") + outsideElement.Call("appendChild", statusElement) +} + +func testFailed(ev testevents.Event) { + document := js.Global().Get("document") + outsideElement := document.Call("getElementById", ev.Name) + + statusElement := document.Call("createElement", "span") + statusElement.Set("textContent", "FAILED") + outsideElement.Call("appendChild", statusElement) +} + +func init() { + testevents.Register(testevents.TestStarted, testevents.EventListener{testStarted, int(testevents.TestStarted)}) + testevents.Register(testevents.TestPassed, testevents.EventListener{testPassed, int(testevents.TestPassed)}) + testevents.Register(testevents.TestFailed, testevents.EventListener{testFailed, int(testevents.TestFailed)}) + + flag.Set("test.v", "true") + flag.Set("test.parallel", "4") +} + +func getWSBaseURL() string { + location := js.Global().Get("window").Get("document").Get("location") + + wsProtocol := "ws" + if location.Get("protocol").String() == "https:" { + wsProtocol = "wss" + } + + return fmt.Sprintf("%s://%s:%s/ws/", wsProtocol, location.Get("hostname").String(), location.Get("port").String()) +} diff --git a/test/test/resources/qunit-1.15.0.css b/test/test/resources/qunit-1.15.0.css deleted file mode 100644 index 9437b4b..0000000 --- a/test/test/resources/qunit-1.15.0.css +++ /dev/null @@ -1,237 +0,0 @@ -/*! - * QUnit 1.15.0 - * http://qunitjs.com/ - * - * Copyright 2014 jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-08-08T16:00Z - */ - -/** Font Family and Sizes */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; -} - -#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } -#qunit-tests { font-size: smaller; } - - -/** Resets */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { - margin: 0; - padding: 0; -} - - -/** Header */ - -#qunit-header { - padding: 0.5em 0 0.5em 1em; - - color: #8699A4; - background-color: #0D3349; - - font-size: 1.5em; - line-height: 1em; - font-weight: 400; - - border-radius: 5px 5px 0 0; -} - -#qunit-header a { - text-decoration: none; - color: #C2CCD1; -} - -#qunit-header a:hover, -#qunit-header a:focus { - color: #FFF; -} - -#qunit-testrunner-toolbar label { - display: inline-block; - padding: 0 0.5em 0 0.1em; -} - -#qunit-banner { - height: 5px; -} - -#qunit-testrunner-toolbar { - padding: 0.5em 1em 0.5em 1em; - color: #5E740B; - background-color: #EEE; - overflow: hidden; -} - -#qunit-userAgent { - padding: 0.5em 1em 0.5em 1em; - background-color: #2B81AF; - color: #FFF; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; -} - -#qunit-modulefilter-container { - float: right; -} - -/** Tests: Pass/Fail */ - -#qunit-tests { - list-style-position: inside; -} - -#qunit-tests li { - padding: 0.4em 1em 0.4em 1em; - border-bottom: 1px solid #FFF; - list-style-position: inside; -} - -#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { - display: none; -} - -#qunit-tests li strong { - cursor: pointer; -} - -#qunit-tests li a { - padding: 0.5em; - color: #C2CCD1; - text-decoration: none; -} -#qunit-tests li a:hover, -#qunit-tests li a:focus { - color: #000; -} - -#qunit-tests li .runtime { - float: right; - font-size: smaller; -} - -.qunit-assert-list { - margin-top: 0.5em; - padding: 0.5em; - - background-color: #FFF; - - border-radius: 5px; -} - -.qunit-collapsed { - display: none; -} - -#qunit-tests table { - border-collapse: collapse; - margin-top: 0.2em; -} - -#qunit-tests th { - text-align: right; - vertical-align: top; - padding: 0 0.5em 0 0; -} - -#qunit-tests td { - vertical-align: top; -} - -#qunit-tests pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; -} - -#qunit-tests del { - background-color: #E0F2BE; - color: #374E0C; - text-decoration: none; -} - -#qunit-tests ins { - background-color: #FFCACA; - color: #500; - text-decoration: none; -} - -/*** Test Counts */ - -#qunit-tests b.counts { color: #000; } -#qunit-tests b.passed { color: #5E740B; } -#qunit-tests b.failed { color: #710909; } - -#qunit-tests li li { - padding: 5px; - background-color: #FFF; - border-bottom: none; - list-style-position: inside; -} - -/*** Passing Styles */ - -#qunit-tests li li.pass { - color: #3C510C; - background-color: #FFF; - border-left: 10px solid #C6E746; -} - -#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } -#qunit-tests .pass .test-name { color: #366097; } - -#qunit-tests .pass .test-actual, -#qunit-tests .pass .test-expected { color: #999; } - -#qunit-banner.qunit-pass { background-color: #C6E746; } - -/*** Failing Styles */ - -#qunit-tests li li.fail { - color: #710909; - background-color: #FFF; - border-left: 10px solid #EE5757; - white-space: pre; -} - -#qunit-tests > li:last-child { - border-radius: 0 0 5px 5px; -} - -#qunit-tests .fail { color: #000; background-color: #EE5757; } -#qunit-tests .fail .test-name, -#qunit-tests .fail .module-name { color: #000; } - -#qunit-tests .fail .test-actual { color: #EE5757; } -#qunit-tests .fail .test-expected { color: #008000; } - -#qunit-banner.qunit-fail { background-color: #EE5757; } - - -/** Result */ - -#qunit-testresult { - padding: 0.5em 1em 0.5em 1em; - - color: #2B81AF; - background-color: #D2E0E6; - - border-bottom: 1px solid #FFF; -} -#qunit-testresult .module-name { - font-weight: 700; -} - -/** Fixture */ - -#qunit-fixture { - position: absolute; - top: -10000px; - left: -10000px; - width: 1000px; - height: 1000px; -} diff --git a/test/test/resources/qunit-1.15.0.js b/test/test/resources/qunit-1.15.0.js deleted file mode 100644 index 474cfe5..0000000 --- a/test/test/resources/qunit-1.15.0.js +++ /dev/null @@ -1,2495 +0,0 @@ -/*! - * QUnit 1.15.0 - * http://qunitjs.com/ - * - * Copyright 2014 jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-08-08T16:00Z - */ - -(function( window ) { - -var QUnit, - config, - onErrorFnPrev, - fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ), - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - // Keep a local reference to Date (GH-283) - Date = window.Date, - now = Date.now || function() { - return new Date().getTime(); - }, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - defined = { - document: typeof window.document !== "undefined", - setTimeout: typeof window.setTimeout !== "undefined", - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch ( e ) { - return false; - } - }()) - }, - /** - * Provides a normalized error string, correcting an issue - * with IE 7 (and prior) where Error.prototype.toString is - * not properly implemented - * - * Based on http://es5.github.com/#x15.11.4.4 - * - * @param {String|Error} error - * @return {String} error message - */ - errorString = function( error ) { - var name, message, - errorString = error.toString(); - if ( errorString.substring( 0, 7 ) === "[object" ) { - name = error.name ? error.name.toString() : "Error"; - message = error.message ? error.message.toString() : ""; - if ( name && message ) { - return name + ": " + message; - } else if ( name ) { - return name; - } else if ( message ) { - return message; - } else { - return "Error"; - } - } else { - return errorString; - } - }, - /** - * Makes a clone of an object using only Array or Object as base, - * and copies over the own enumerable properties. - * - * @param {Object} obj - * @return {Object} New object with only the own properties (recursively). - */ - objectValues = function( obj ) { - var key, val, - vals = QUnit.is( "array", obj ) ? [] : {}; - for ( key in obj ) { - if ( hasOwn.call( obj, key ) ) { - val = obj[ key ]; - vals[ key ] = val === Object( val ) ? objectValues( val ) : val; - } - } - return vals; - }; - -// Root QUnit object. -// `QUnit` initialized at top of scope -QUnit = { - - // call on start of module test to prepend name to all tests - module: function( name, testEnvironment ) { - config.currentModule = name; - config.currentModuleTestEnvironment = testEnvironment; - config.modules[ name ] = true; - }, - - asyncTest: function( testName, expected, callback ) { - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - QUnit.test( testName, expected, callback, true ); - }, - - test: function( testName, expected, callback, async ) { - var test; - - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - test = new Test({ - testName: testName, - expected: expected, - async: async, - callback: callback, - module: config.currentModule, - moduleTestEnvironment: config.currentModuleTestEnvironment, - stack: sourceFromStacktrace( 2 ) - }); - - if ( !validTest( test ) ) { - return; - } - - test.queue(); - }, - - start: function( count ) { - var message; - - // QUnit hasn't been initialized yet. - // Note: RequireJS (et al) may delay onLoad - if ( config.semaphore === undefined ) { - QUnit.begin(function() { - // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first - setTimeout(function() { - QUnit.start( count ); - }); - }); - return; - } - - config.semaphore -= count || 1; - // don't start until equal number of stop-calls - if ( config.semaphore > 0 ) { - return; - } - - // Set the starting time when the first test is run - QUnit.config.started = QUnit.config.started || now(); - // ignore if start is called more often then stop - if ( config.semaphore < 0 ) { - config.semaphore = 0; - - message = "Called start() while already started (QUnit.config.semaphore was 0 already)"; - - if ( config.current ) { - QUnit.pushFailure( message, sourceFromStacktrace( 2 ) ); - } else { - throw new Error( message ); - } - - return; - } - // A slight delay, to avoid any current callbacks - if ( defined.setTimeout ) { - setTimeout(function() { - if ( config.semaphore > 0 ) { - return; - } - if ( config.timeout ) { - clearTimeout( config.timeout ); - } - - config.blocking = false; - process( true ); - }, 13 ); - } else { - config.blocking = false; - process( true ); - } - }, - - stop: function( count ) { - config.semaphore += count || 1; - config.blocking = true; - - if ( config.testTimeout && defined.setTimeout ) { - clearTimeout( config.timeout ); - config.timeout = setTimeout(function() { - QUnit.ok( false, "Test timed out" ); - config.semaphore = 1; - QUnit.start(); - }, config.testTimeout ); - } - } -}; - -// We use the prototype to distinguish between properties that should -// be exposed as globals (and in exports) and those that shouldn't -(function() { - function F() {} - F.prototype = QUnit; - QUnit = new F(); - - // Make F QUnit's constructor so that we can add to the prototype later - QUnit.constructor = F; -}()); - -/** - * Config object: Maintain internal state - * Later exposed as QUnit.config - * `config` initialized at top of scope - */ -config = { - // The queue of tests to run - queue: [], - - // block until document ready - blocking: true, - - // when enabled, show only failing tests - // gets persisted through sessionStorage and can be changed in UI via checkbox - hidepassed: false, - - // by default, run previously failed tests first - // very useful in combination with "Hide passed tests" checked - reorder: true, - - // by default, modify document.title when suite is done - altertitle: true, - - // by default, scroll to top of the page when suite is done - scrolltop: true, - - // when enabled, all tests must call expect() - requireExpects: false, - - // add checkboxes that are persisted in the query-string - // when enabled, the id is set to `true` as a `QUnit.config` property - urlConfig: [ - { - id: "noglobals", - label: "Check for Globals", - tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." - }, - { - id: "notrycatch", - label: "No try-catch", - tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." - } - ], - - // Set of all modules. - modules: {}, - - callbacks: {} -}; - -// Initialize more QUnit.config and QUnit.urlParams -(function() { - var i, current, - location = window.location || { search: "", protocol: "file:" }, - params = location.search.slice( 1 ).split( "&" ), - length = params.length, - urlParams = {}; - - if ( params[ 0 ] ) { - for ( i = 0; i < length; i++ ) { - current = params[ i ].split( "=" ); - current[ 0 ] = decodeURIComponent( current[ 0 ] ); - - // allow just a key to turn on a flag, e.g., test.html?noglobals - current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; - if ( urlParams[ current[ 0 ] ] ) { - urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); - } else { - urlParams[ current[ 0 ] ] = current[ 1 ]; - } - } - } - - QUnit.urlParams = urlParams; - - // String search anywhere in moduleName+testName - config.filter = urlParams.filter; - - // Exact match of the module name - config.module = urlParams.module; - - config.testNumber = []; - if ( urlParams.testNumber ) { - - // Ensure that urlParams.testNumber is an array - urlParams.testNumber = [].concat( urlParams.testNumber ); - for ( i = 0; i < urlParams.testNumber.length; i++ ) { - current = urlParams.testNumber[ i ]; - config.testNumber.push( parseInt( current, 10 ) ); - } - } - - // Figure out if we're running the tests from a server or not - QUnit.isLocal = location.protocol === "file:"; -}()); - -extend( QUnit, { - - config: config, - - // Safe object type checking - is: function( type, obj ) { - return QUnit.objectType( obj ) === type; - }, - - objectType: function( obj ) { - if ( typeof obj === "undefined" ) { - return "undefined"; - } - - // Consider: typeof null === object - if ( obj === null ) { - return "null"; - } - - var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), - type = match && match[ 1 ] || ""; - - switch ( type ) { - case "Number": - if ( isNaN( obj ) ) { - return "nan"; - } - return "number"; - case "String": - case "Boolean": - case "Array": - case "Date": - case "RegExp": - case "Function": - return type.toLowerCase(); - } - if ( typeof obj === "object" ) { - return "object"; - } - return undefined; - }, - - url: function( params ) { - params = extend( extend( {}, QUnit.urlParams ), params ); - var key, - querystring = "?"; - - for ( key in params ) { - if ( hasOwn.call( params, key ) ) { - querystring += encodeURIComponent( key ) + "=" + - encodeURIComponent( params[ key ] ) + "&"; - } - } - return window.location.protocol + "//" + window.location.host + - window.location.pathname + querystring.slice( 0, -1 ); - }, - - extend: extend -}); - -/** - * @deprecated: Created for backwards compatibility with test runner that set the hook function - * into QUnit.{hook}, instead of invoking it and passing the hook function. - * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. - * Doing this allows us to tell if the following methods have been overwritten on the actual - * QUnit object. - */ -extend( QUnit.constructor.prototype, { - - // Logging callbacks; all receive a single argument with the listed properties - // run test/logs.html for any related changes - begin: registerLoggingCallback( "begin" ), - - // done: { failed, passed, total, runtime } - done: registerLoggingCallback( "done" ), - - // log: { result, actual, expected, message } - log: registerLoggingCallback( "log" ), - - // testStart: { name } - testStart: registerLoggingCallback( "testStart" ), - - // testDone: { name, failed, passed, total, runtime } - testDone: registerLoggingCallback( "testDone" ), - - // moduleStart: { name } - moduleStart: registerLoggingCallback( "moduleStart" ), - - // moduleDone: { name, failed, passed, total } - moduleDone: registerLoggingCallback( "moduleDone" ) -}); - -QUnit.load = function() { - runLoggingCallbacks( "begin", { - totalTests: Test.count - }); - - // Initialize the configuration options - extend( config, { - stats: { all: 0, bad: 0 }, - moduleStats: { all: 0, bad: 0 }, - started: 0, - updateRate: 1000, - autostart: true, - filter: "", - semaphore: 1 - }, true ); - - config.blocking = false; - - if ( config.autostart ) { - QUnit.start(); - } -}; - -// `onErrorFnPrev` initialized at top of scope -// Preserve other handlers -onErrorFnPrev = window.onerror; - -// Cover uncaught exceptions -// Returning true will suppress the default browser handler, -// returning false will let it run. -window.onerror = function( error, filePath, linerNr ) { - var ret = false; - if ( onErrorFnPrev ) { - ret = onErrorFnPrev( error, filePath, linerNr ); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if ( ret !== true ) { - if ( QUnit.config.current ) { - if ( QUnit.config.current.ignoreGlobalErrors ) { - return true; - } - QUnit.pushFailure( error, filePath + ":" + linerNr ); - } else { - QUnit.test( "global failure", extend(function() { - QUnit.pushFailure( error, filePath + ":" + linerNr ); - }, { validTest: validTest } ) ); - } - return false; - } - - return ret; -}; - -function done() { - config.autorun = true; - - // Log the last module results - if ( config.previousModule ) { - runLoggingCallbacks( "moduleDone", { - name: config.previousModule, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all - }); - } - delete config.previousModule; - - var runtime = now() - config.started, - passed = config.stats.all - config.stats.bad; - - runLoggingCallbacks( "done", { - failed: config.stats.bad, - passed: passed, - total: config.stats.all, - runtime: runtime - }); -} - -/** @return Boolean: true if this test should be ran */ -function validTest( test ) { - var include, - filter = config.filter && config.filter.toLowerCase(), - module = config.module && config.module.toLowerCase(), - fullName = ( test.module + ": " + test.testName ).toLowerCase(); - - // Internally-generated tests are always valid - if ( test.callback && test.callback.validTest === validTest ) { - delete test.callback.validTest; - return true; - } - - if ( config.testNumber.length > 0 ) { - if ( inArray( test.testNumber, config.testNumber ) < 0 ) { - return false; - } - } - - if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { - return false; - } - - if ( !filter ) { - return true; - } - - include = filter.charAt( 0 ) !== "!"; - if ( !include ) { - filter = filter.slice( 1 ); - } - - // If the filter matches, we need to honour include - if ( fullName.indexOf( filter ) !== -1 ) { - return include; - } - - // Otherwise, do the opposite - return !include; -} - -// Doesn't support IE6 to IE9 -// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack -function extractStacktrace( e, offset ) { - offset = offset === undefined ? 4 : offset; - - var stack, include, i; - - if ( e.stacktrace ) { - - // Opera 12.x - return e.stacktrace.split( "\n" )[ offset + 3 ]; - } else if ( e.stack ) { - - // Firefox, Chrome, Safari 6+, IE10+, PhantomJS and Node - stack = e.stack.split( "\n" ); - if ( /^error$/i.test( stack[ 0 ] ) ) { - stack.shift(); - } - if ( fileName ) { - include = []; - for ( i = offset; i < stack.length; i++ ) { - if ( stack[ i ].indexOf( fileName ) !== -1 ) { - break; - } - include.push( stack[ i ] ); - } - if ( include.length ) { - return include.join( "\n" ); - } - } - return stack[ offset ]; - } else if ( e.sourceURL ) { - - // Safari < 6 - // exclude useless self-reference for generated Error objects - if ( /qunit.js$/.test( e.sourceURL ) ) { - return; - } - - // for actual exceptions, this is useful - return e.sourceURL + ":" + e.line; - } -} -function sourceFromStacktrace( offset ) { - try { - throw new Error(); - } catch ( e ) { - return extractStacktrace( e, offset ); - } -} - -function synchronize( callback, last ) { - config.queue.push( callback ); - - if ( config.autorun && !config.blocking ) { - process( last ); - } -} - -function process( last ) { - function next() { - process( last ); - } - var start = now(); - config.depth = config.depth ? config.depth + 1 : 1; - - while ( config.queue.length && !config.blocking ) { - if ( !defined.setTimeout || config.updateRate <= 0 || ( ( now() - start ) < config.updateRate ) ) { - config.queue.shift()(); - } else { - setTimeout( next, 13 ); - break; - } - } - config.depth--; - if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { - done(); - } -} - -function saveGlobal() { - config.pollution = []; - - if ( config.noglobals ) { - for ( var key in window ) { - if ( hasOwn.call( window, key ) ) { - // in Opera sometimes DOM element ids show up here, ignore them - if ( /^qunit-test-output/.test( key ) ) { - continue; - } - config.pollution.push( key ); - } - } - } -} - -function checkPollution() { - var newGlobals, - deletedGlobals, - old = config.pollution; - - saveGlobal(); - - newGlobals = diff( config.pollution, old ); - if ( newGlobals.length > 0 ) { - QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); - } - - deletedGlobals = diff( old, config.pollution ); - if ( deletedGlobals.length > 0 ) { - QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); - } -} - -// returns a new Array with the elements that are in a but not in b -function diff( a, b ) { - var i, j, - result = a.slice(); - - for ( i = 0; i < result.length; i++ ) { - for ( j = 0; j < b.length; j++ ) { - if ( result[ i ] === b[ j ] ) { - result.splice( i, 1 ); - i--; - break; - } - } - } - return result; -} - -function extend( a, b, undefOnly ) { - for ( var prop in b ) { - if ( hasOwn.call( b, prop ) ) { - - // Avoid "Member not found" error in IE8 caused by messing with window.constructor - if ( !( prop === "constructor" && a === window ) ) { - if ( b[ prop ] === undefined ) { - delete a[ prop ]; - } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { - a[ prop ] = b[ prop ]; - } - } - } - } - - return a; -} - -function registerLoggingCallback( key ) { - - // Initialize key collection of logging callback - if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { - config.callbacks[ key ] = []; - } - - return function( callback ) { - config.callbacks[ key ].push( callback ); - }; -} - -function runLoggingCallbacks( key, args ) { - var i, l, callbacks; - - callbacks = config.callbacks[ key ]; - for ( i = 0, l = callbacks.length; i < l; i++ ) { - callbacks[ i ]( args ); - } -} - -// from jquery.js -function inArray( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } - - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; - } - } - - return -1; -} - -function Test( settings ) { - extend( this, settings ); - this.assert = new Assert( this ); - this.assertions = []; - this.testNumber = ++Test.count; -} - -Test.count = 0; - -Test.prototype = { - setup: function() { - if ( - - // Emit moduleStart when we're switching from one module to another - this.module !== config.previousModule || - - // They could be equal (both undefined) but if the previousModule property doesn't - // yet exist it means this is the first test in a suite that isn't wrapped in a - // module, in which case we'll just emit a moduleStart event for 'undefined'. - // Without this, reporters can get testStart before moduleStart which is a problem. - !hasOwn.call( config, "previousModule" ) - ) { - if ( hasOwn.call( config, "previousModule" ) ) { - runLoggingCallbacks( "moduleDone", { - name: config.previousModule, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all - }); - } - config.previousModule = this.module; - config.moduleStats = { all: 0, bad: 0 }; - runLoggingCallbacks( "moduleStart", { - name: this.module - }); - } - - config.current = this; - - this.testEnvironment = extend({ - setup: function() {}, - teardown: function() {} - }, this.moduleTestEnvironment ); - - this.started = now(); - runLoggingCallbacks( "testStart", { - name: this.testName, - module: this.module, - testNumber: this.testNumber - }); - - if ( !config.pollution ) { - saveGlobal(); - } - if ( config.notrycatch ) { - this.testEnvironment.setup.call( this.testEnvironment, this.assert ); - return; - } - try { - this.testEnvironment.setup.call( this.testEnvironment, this.assert ); - } catch ( e ) { - this.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); - } - }, - run: function() { - config.current = this; - - if ( this.async ) { - QUnit.stop(); - } - - this.callbackStarted = now(); - - if ( config.notrycatch ) { - this.callback.call( this.testEnvironment, this.assert ); - this.callbackRuntime = now() - this.callbackStarted; - return; - } - - try { - this.callback.call( this.testEnvironment, this.assert ); - this.callbackRuntime = now() - this.callbackStarted; - } catch ( e ) { - this.callbackRuntime = now() - this.callbackStarted; - - this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); - - // else next test will carry the responsibility - saveGlobal(); - - // Restart the tests if they're blocking - if ( config.blocking ) { - QUnit.start(); - } - } - }, - teardown: function() { - config.current = this; - if ( config.notrycatch ) { - if ( typeof this.callbackRuntime === "undefined" ) { - this.callbackRuntime = now() - this.callbackStarted; - } - this.testEnvironment.teardown.call( this.testEnvironment, this.assert ); - return; - } else { - try { - this.testEnvironment.teardown.call( this.testEnvironment, this.assert ); - } catch ( e ) { - this.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); - } - } - checkPollution(); - }, - finish: function() { - config.current = this; - if ( config.requireExpects && this.expected === null ) { - this.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); - } else if ( this.expected !== null && this.expected !== this.assertions.length ) { - this.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); - } else if ( this.expected === null && !this.assertions.length ) { - this.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); - } - - var i, - bad = 0; - - this.runtime = now() - this.started; - config.stats.all += this.assertions.length; - config.moduleStats.all += this.assertions.length; - - for ( i = 0; i < this.assertions.length; i++ ) { - if ( !this.assertions[ i ].result ) { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - - runLoggingCallbacks( "testDone", { - name: this.testName, - module: this.module, - failed: bad, - passed: this.assertions.length - bad, - total: this.assertions.length, - runtime: this.runtime, - - // HTML Reporter use - assertions: this.assertions, - testNumber: this.testNumber, - - // DEPRECATED: this property will be removed in 2.0.0, use runtime instead - duration: this.runtime - }); - - config.current = undefined; - }, - - queue: function() { - var bad, - test = this; - - function run() { - // each of these can by async - synchronize(function() { - test.setup(); - }); - synchronize(function() { - test.run(); - }); - synchronize(function() { - test.teardown(); - }); - synchronize(function() { - test.finish(); - }); - } - - // `bad` initialized at top of scope - // defer when previous test run passed, if storage is available - bad = QUnit.config.reorder && defined.sessionStorage && - +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); - - if ( bad ) { - run(); - } else { - synchronize( run, true ); - } - }, - - push: function( result, actual, expected, message ) { - var source, - details = { - module: this.module, - name: this.testName, - result: result, - message: message, - actual: actual, - expected: expected, - testNumber: this.testNumber - }; - - if ( !result ) { - source = sourceFromStacktrace(); - - if ( source ) { - details.source = source; - } - } - - runLoggingCallbacks( "log", details ); - - this.assertions.push({ - result: !!result, - message: message - }); - }, - - pushFailure: function( message, source, actual ) { - if ( !this instanceof Test ) { - throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace( 2 ) ); - } - - var details = { - module: this.module, - name: this.testName, - result: false, - message: message || "error", - actual: actual || null, - testNumber: this.testNumber - }; - - if ( source ) { - details.source = source; - } - - runLoggingCallbacks( "log", details ); - - this.assertions.push({ - result: false, - message: message - }); - } -}; - -QUnit.pushFailure = function() { - if ( !QUnit.config.current ) { - throw new Error( "pushFailure() assertion outside test context, in " + sourceFromStacktrace( 2 ) ); - } - - // Gets current test obj - var currentTest = QUnit.config.current.assert.test; - - return currentTest.pushFailure.apply( currentTest, arguments ); -}; - -function Assert( testContext ) { - this.test = testContext; -} - -// Assert helpers -QUnit.assert = Assert.prototype = { - - // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. - expect: function( asserts ) { - if ( arguments.length === 1 ) { - this.test.expected = asserts; - } else { - return this.test.expected; - } - }, - - // Exports test.push() to the user API - push: function() { - var assert = this; - - // Backwards compatibility fix. - // Allows the direct use of global exported assertions and QUnit.assert.* - // Although, it's use is not recommended as it can leak assertions - // to other tests from async tests, because we only get a reference to the current test, - // not exactly the test where assertion were intended to be called. - if ( !QUnit.config.current ) { - throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); - } - if ( !( assert instanceof Assert ) ) { - assert = QUnit.config.current.assert; - } - return assert.test.push.apply( assert.test, arguments ); - }, - - /** - * Asserts rough true-ish result. - * @name ok - * @function - * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); - */ - ok: function( result, message ) { - message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + - QUnit.dump.parse( result ) ); - if ( !!result ) { - this.push( true, result, true, message ); - } else { - this.test.pushFailure( message, null, result ); - } - }, - - /** - * Assert that the first two arguments are equal, with an optional message. - * Prints out both actual and expected values. - * @name equal - * @function - * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); - */ - equal: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - this.push( expected == actual, actual, expected, message ); - }, - - /** - * @name notEqual - * @function - */ - notEqual: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - this.push( expected != actual, actual, expected, message ); - }, - - /** - * @name propEqual - * @function - */ - propEqual: function( actual, expected, message ) { - actual = objectValues( actual ); - expected = objectValues( expected ); - this.push( QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - /** - * @name notPropEqual - * @function - */ - notPropEqual: function( actual, expected, message ) { - actual = objectValues( actual ); - expected = objectValues( expected ); - this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - /** - * @name deepEqual - * @function - */ - deepEqual: function( actual, expected, message ) { - this.push( QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - /** - * @name notDeepEqual - * @function - */ - notDeepEqual: function( actual, expected, message ) { - this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - /** - * @name strictEqual - * @function - */ - strictEqual: function( actual, expected, message ) { - this.push( expected === actual, actual, expected, message ); - }, - - /** - * @name notStrictEqual - * @function - */ - notStrictEqual: function( actual, expected, message ) { - this.push( expected !== actual, actual, expected, message ); - }, - - "throws": function( block, expected, message ) { - var actual, expectedType, - expectedOutput = expected, - ok = false; - - // 'expected' is optional unless doing string comparison - if ( message == null && typeof expected === "string" ) { - message = expected; - expected = null; - } - - this.test.ignoreGlobalErrors = true; - try { - block.call( this.test.testEnvironment ); - } catch (e) { - actual = e; - } - this.test.ignoreGlobalErrors = false; - - if ( actual ) { - expectedType = QUnit.objectType( expected ); - - // we don't want to validate thrown error - if ( !expected ) { - ok = true; - expectedOutput = null; - - // expected is a regexp - } else if ( expectedType === "regexp" ) { - ok = expected.test( errorString( actual ) ); - - // expected is a string - } else if ( expectedType === "string" ) { - ok = expected === errorString( actual ); - - // expected is a constructor, maybe an Error constructor - } else if ( expectedType === "function" && actual instanceof expected ) { - ok = true; - - // expected is an Error object - } else if ( expectedType === "object" ) { - ok = actual instanceof expected.constructor && - actual.name === expected.name && - actual.message === expected.message; - - // expected is a validation function which returns true if validation passed - } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { - expectedOutput = null; - ok = true; - } - - this.push( ok, actual, expectedOutput, message ); - } else { - this.test.pushFailure( message, null, "No exception was thrown." ); - } - } -}; - -// Test for equality any JavaScript type. -// Author: Philippe Rathé -QUnit.equiv = (function() { - - // Call the o related callback with the given arguments. - function bindCallbacks( o, callbacks, args ) { - var prop = QUnit.objectType( o ); - if ( prop ) { - if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { - return callbacks[ prop ].apply( callbacks, args ); - } else { - return callbacks[ prop ]; // or undefined - } - } - } - - // the real equiv function - var innerEquiv, - - // stack to decide between skip/abort functions - callers = [], - - // stack to avoiding loops from circular referencing - parents = [], - parentsB = [], - - getProto = Object.getPrototypeOf || function( obj ) { - /* jshint camelcase: false, proto: true */ - return obj.__proto__; - }, - callbacks = (function() { - - // for string, boolean, number and null - function useStrictEquality( b, a ) { - - /*jshint eqeqeq:false */ - if ( b instanceof a.constructor || a instanceof b.constructor ) { - - // to catch short annotation VS 'new' annotation of a - // declaration - // e.g. var i = 1; - // var j = new Number(1); - return a == b; - } else { - return a === b; - } - } - - return { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, - - "nan": function( b ) { - return isNaN( b ); - }, - - "date": function( b, a ) { - return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); - }, - - "regexp": function( b, a ) { - return QUnit.objectType( b ) === "regexp" && - - // the regex itself - a.source === b.source && - - // and its modifiers - a.global === b.global && - - // (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline && - a.sticky === b.sticky; - }, - - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function() { - var caller = callers[ callers.length - 1 ]; - return caller !== Object && typeof caller !== "undefined"; - }, - - "array": function( b, a ) { - var i, j, len, loop, aCircular, bCircular; - - // b could be an object literal here - if ( QUnit.objectType( b ) !== "array" ) { - return false; - } - - len = a.length; - if ( len !== b.length ) { - // safe and faster - return false; - } - - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - for ( i = 0; i < len; i++ ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - parents.pop(); - parentsB.pop(); - return false; - } - } - } - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { - parents.pop(); - parentsB.pop(); - return false; - } - } - parents.pop(); - parentsB.pop(); - return true; - }, - - "object": function( b, a ) { - - /*jshint forin:false */ - var i, j, loop, aCircular, bCircular, - // Default to true - eq = true, - aProperties = [], - bProperties = []; - - // comparing constructors is more strict than using - // instanceof - if ( a.constructor !== b.constructor ) { - - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || - ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { - return false; - } - } - - // stack constructor before traversing properties - callers.push( a.constructor ); - - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - - // be strict: don't ensure hasOwnProperty and go deep - for ( i in a ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - eq = false; - break; - } - } - } - aProperties.push( i ); - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { - eq = false; - break; - } - } - - parents.pop(); - parentsB.pop(); - callers.pop(); // unstack, we are done - - for ( i in b ) { - bProperties.push( i ); // collect b's properties - } - - // Ensures identical properties name - return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); - } - }; - }()); - - innerEquiv = function() { // can take multiple arguments - var args = [].slice.apply( arguments ); - if ( args.length < 2 ) { - return true; // end transition - } - - return ( (function( a, b ) { - if ( a === b ) { - return true; // catch the most you can - } else if ( a === null || b === null || typeof a === "undefined" || - typeof b === "undefined" || - QUnit.objectType( a ) !== QUnit.objectType( b ) ) { - - // don't lose time with error prone cases - return false; - } else { - return bindCallbacks( a, callbacks, [ b, a ] ); - } - - // apply transition with (1..n) arguments - }( args[ 0 ], args[ 1 ] ) ) && innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); - }; - - return innerEquiv; -}()); - -// Based on jsDump by Ariel Flesler -// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html -QUnit.dump = (function() { - function quote( str ) { - return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; - } - function literal( o ) { - return o + ""; - } - function join( pre, arr, post ) { - var s = dump.separator(), - base = dump.indent(), - inner = dump.indent( 1 ); - if ( arr.join ) { - arr = arr.join( "," + s + inner ); - } - if ( !arr ) { - return pre + post; - } - return [ pre, inner + arr, base + post ].join( s ); - } - function array( arr, stack ) { - var i = arr.length, - ret = new Array( i ); - this.up(); - while ( i-- ) { - ret[ i ] = this.parse( arr[ i ], undefined, stack ); - } - this.down(); - return join( "[", ret, "]" ); - } - - var reName = /^function (\w+)/, - dump = { - // type is used mostly internally, you can fix a (custom)type in advance - parse: function( obj, type, stack ) { - stack = stack || []; - var inStack, res, - parser = this.parsers[ type || this.typeOf( obj ) ]; - - type = typeof parser; - inStack = inArray( obj, stack ); - - if ( inStack !== -1 ) { - return "recursion(" + ( inStack - stack.length ) + ")"; - } - if ( type === "function" ) { - stack.push( obj ); - res = parser.call( this, obj, stack ); - stack.pop(); - return res; - } - return ( type === "string" ) ? parser : this.parsers.error; - }, - typeOf: function( obj ) { - var type; - if ( obj === null ) { - type = "null"; - } else if ( typeof obj === "undefined" ) { - type = "undefined"; - } else if ( QUnit.is( "regexp", obj ) ) { - type = "regexp"; - } else if ( QUnit.is( "date", obj ) ) { - type = "date"; - } else if ( QUnit.is( "function", obj ) ) { - type = "function"; - } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { - type = "window"; - } else if ( obj.nodeType === 9 ) { - type = "document"; - } else if ( obj.nodeType ) { - type = "node"; - } else if ( - - // native arrays - toString.call( obj ) === "[object Array]" || - - // NodeList objects - ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && typeof obj[ 0 ] === "undefined" ) ) ) - ) { - type = "array"; - } else if ( obj.constructor === Error.prototype.constructor ) { - type = "error"; - } else { - type = typeof obj; - } - return type; - }, - separator: function() { - return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; - }, - // extra can be a number, shortcut for increasing-calling-decreasing - indent: function( extra ) { - if ( !this.multiline ) { - return ""; - } - var chr = this.indentChar; - if ( this.HTML ) { - chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); - } - return new Array( this.depth + ( extra || 0 ) ).join( chr ); - }, - up: function( a ) { - this.depth += a || 1; - }, - down: function( a ) { - this.depth -= a || 1; - }, - setParser: function( name, parser ) { - this.parsers[ name ] = parser; - }, - // The next 3 are exposed so you can use them - quote: quote, - literal: literal, - join: join, - // - depth: 1, - // This is the list of parsers, to modify them, use dump.setParser - parsers: { - window: "[Window]", - document: "[Document]", - error: function( error ) { - return "Error(\"" + error.message + "\")"; - }, - unknown: "[Unknown]", - "null": "null", - "undefined": "undefined", - "function": function( fn ) { - var ret = "function", - // functions never have name in IE - name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; - - if ( name ) { - ret += " " + name; - } - ret += "( "; - - ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); - return join( ret, dump.parse( fn, "functionCode" ), "}" ); - }, - array: array, - nodelist: array, - "arguments": array, - object: function( map, stack ) { - /*jshint forin:false */ - var ret = [], keys, key, val, i, nonEnumerableProperties; - dump.up(); - keys = []; - for ( key in map ) { - keys.push( key ); - } - - // Some properties are not always enumerable on Error objects. - nonEnumerableProperties = [ "message", "name" ]; - for ( i in nonEnumerableProperties ) { - key = nonEnumerableProperties[ i ]; - if ( key in map && !( key in keys ) ) { - keys.push( key ); - } - } - keys.sort(); - for ( i = 0; i < keys.length; i++ ) { - key = keys[ i ]; - val = map[ key ]; - ret.push( dump.parse( key, "key" ) + ": " + dump.parse( val, undefined, stack ) ); - } - dump.down(); - return join( "{", ret, "}" ); - }, - node: function( node ) { - var len, i, val, - open = dump.HTML ? "<" : "<", - close = dump.HTML ? ">" : ">", - tag = node.nodeName.toLowerCase(), - ret = open + tag, - attrs = node.attributes; - - if ( attrs ) { - for ( i = 0, len = attrs.length; i < len; i++ ) { - val = attrs[ i ].nodeValue; - - // IE6 includes all attributes in .attributes, even ones not explicitly set. - // Those have values like undefined, null, 0, false, "" or "inherit". - if ( val && val !== "inherit" ) { - ret += " " + attrs[ i ].nodeName + "=" + dump.parse( val, "attribute" ); - } - } - } - ret += close; - - // Show content of TextNode or CDATASection - if ( node.nodeType === 3 || node.nodeType === 4 ) { - ret += node.nodeValue; - } - - return ret + open + "/" + tag + close; - }, - - // function calls it internally, it's the arguments part of the function - functionArgs: function( fn ) { - var args, - l = fn.length; - - if ( !l ) { - return ""; - } - - args = new Array( l ); - while ( l-- ) { - - // 97 is 'a' - args[ l ] = String.fromCharCode( 97 + l ); - } - return " " + args.join( ", " ) + " "; - }, - // object calls it internally, the key part of an item in a map - key: quote, - // function calls it internally, it's the content of the function - functionCode: "[code]", - // node calls it internally, it's an html attribute value - attribute: quote, - string: quote, - date: quote, - regexp: literal, - number: literal, - "boolean": literal - }, - // if true, entities are escaped ( <, >, \t, space and \n ) - HTML: false, - // indentation unit - indentChar: " ", - // if true, items in a collection, are separated by a \n, else just a space. - multiline: true - }; - - return dump; -}()); - -// back compat -QUnit.jsDump = QUnit.dump; - -// For browser, export only select globals -if ( typeof window !== "undefined" ) { - - // Deprecated - // Extend assert methods to QUnit and Global scope through Backwards compatibility - (function() { - var i, - assertions = Assert.prototype; - - function applyCurrent( current ) { - return function() { - var assert = new Assert( QUnit.config.current ); - current.apply( assert, arguments ); - }; - } - - for ( i in assertions ) { - QUnit[ i ] = applyCurrent( assertions[ i ] ); - } - })(); - - (function() { - var i, l, - keys = [ - "test", - "module", - "expect", - "asyncTest", - "start", - "stop", - "ok", - "equal", - "notEqual", - "propEqual", - "notPropEqual", - "deepEqual", - "notDeepEqual", - "strictEqual", - "notStrictEqual", - "throws" - ]; - - for ( i = 0, l = keys.length; i < l; i++ ) { - window[ keys[ i ] ] = QUnit[ keys[ i ] ]; - } - })(); - - window.QUnit = QUnit; -} - -// For CommonJS environments, export everything -if ( typeof module !== "undefined" && module.exports ) { - module.exports = QUnit; -} - -// Get a reference to the global object, like window in browsers -}( (function() { - return this; -})() )); - -/*istanbul ignore next */ -/* - * Javascript Diff Algorithm - * By John Resig (http://ejohn.org/) - * Modified by Chu Alan "sprite" - * - * Released under the MIT license. - * - * More Info: - * http://ejohn.org/projects/javascript-diff-algorithm/ - * - * Usage: QUnit.diff(expected, actual) - * - * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" - */ -QUnit.diff = (function() { - var hasOwn = Object.prototype.hasOwnProperty; - - /*jshint eqeqeq:false, eqnull:true */ - function diff( o, n ) { - var i, - ns = {}, - os = {}; - - for ( i = 0; i < n.length; i++ ) { - if ( !hasOwn.call( ns, n[ i ] ) ) { - ns[ n[ i ] ] = { - rows: [], - o: null - }; - } - ns[ n[ i ] ].rows.push( i ); - } - - for ( i = 0; i < o.length; i++ ) { - if ( !hasOwn.call( os, o[ i ] ) ) { - os[ o[ i ] ] = { - rows: [], - n: null - }; - } - os[ o[ i ] ].rows.push( i ); - } - - for ( i in ns ) { - if ( hasOwn.call( ns, i ) ) { - if ( ns[ i ].rows.length === 1 && hasOwn.call( os, i ) && os[ i ].rows.length === 1 ) { - n[ ns[ i ].rows[ 0 ] ] = { - text: n[ ns[ i ].rows[ 0 ] ], - row: os[ i ].rows[ 0 ] - }; - o[ os[ i ].rows[ 0 ] ] = { - text: o[ os[ i ].rows[ 0 ] ], - row: ns[ i ].rows[ 0 ] - }; - } - } - } - - for ( i = 0; i < n.length - 1; i++ ) { - if ( n[ i ].text != null && n[ i + 1 ].text == null && n[ i ].row + 1 < o.length && o[ n[ i ].row + 1 ].text == null && - n[ i + 1 ] == o[ n[ i ].row + 1 ] ) { - - n[ i + 1 ] = { - text: n[ i + 1 ], - row: n[ i ].row + 1 - }; - o[ n[ i ].row + 1 ] = { - text: o[ n[ i ].row + 1 ], - row: i + 1 - }; - } - } - - for ( i = n.length - 1; i > 0; i-- ) { - if ( n[ i ].text != null && n[ i - 1 ].text == null && n[ i ].row > 0 && o[ n[ i ].row - 1 ].text == null && - n[ i - 1 ] == o[ n[ i ].row - 1 ] ) { - - n[ i - 1 ] = { - text: n[ i - 1 ], - row: n[ i ].row - 1 - }; - o[ n[ i ].row - 1 ] = { - text: o[ n[ i ].row - 1 ], - row: i - 1 - }; - } - } - - return { - o: o, - n: n - }; - } - - return function( o, n ) { - o = o.replace( /\s+$/, "" ); - n = n.replace( /\s+$/, "" ); - - var i, pre, - str = "", - out = diff( o === "" ? [] : o.split( /\s+/ ), n === "" ? [] : n.split( /\s+/ ) ), - oSpace = o.match( /\s+/g ), - nSpace = n.match( /\s+/g ); - - if ( oSpace == null ) { - oSpace = [ " " ]; - } else { - oSpace.push( " " ); - } - - if ( nSpace == null ) { - nSpace = [ " " ]; - } else { - nSpace.push( " " ); - } - - if ( out.n.length === 0 ) { - for ( i = 0; i < out.o.length; i++ ) { - str += "" + out.o[ i ] + oSpace[ i ] + ""; - } - } else { - if ( out.n[ 0 ].text == null ) { - for ( n = 0; n < out.o.length && out.o[ n ].text == null; n++ ) { - str += "" + out.o[ n ] + oSpace[ n ] + ""; - } - } - - for ( i = 0; i < out.n.length; i++ ) { - if ( out.n[ i ].text == null ) { - str += "" + out.n[ i ] + nSpace[ i ] + ""; - } else { - - // `pre` initialized at top of scope - pre = ""; - - for ( n = out.n[ i ].row + 1; n < out.o.length && out.o[ n ].text == null; n++ ) { - pre += "" + out.o[ n ] + oSpace[ n ] + ""; - } - str += " " + out.n[ i ].text + nSpace[ i ] + pre; - } - } - } - - return str; - }; -}()); - -(function() { - -// Deprecated QUnit.init - Ref #530 -// Re-initialize the configuration options -QUnit.init = function() { - var tests, banner, result, qunit, - config = QUnit.config; - - config.stats = { all: 0, bad: 0 }; - config.moduleStats = { all: 0, bad: 0 }; - config.started = 0; - config.updateRate = 1000; - config.blocking = false; - config.autostart = true; - config.autorun = false; - config.filter = ""; - config.queue = []; - config.semaphore = 1; - - // Return on non-browser environments - // This is necessary to not break on node tests - if ( typeof window === "undefined" ) { - return; - } - - qunit = id( "qunit" ); - if ( qunit ) { - qunit.innerHTML = - "

" + escapeText( document.title ) + "

" + - "

" + - "
" + - "

" + - "
    "; - } - - tests = id( "qunit-tests" ); - banner = id( "qunit-banner" ); - result = id( "qunit-testresult" ); - - if ( tests ) { - tests.innerHTML = ""; - } - - if ( banner ) { - banner.className = ""; - } - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = "Running...
     "; - } -}; - -// Resets the test setup. Useful for tests that modify the DOM. -/* -DEPRECATED: Use multiple tests instead of resetting inside a test. -Use testStart or testDone for custom cleanup. -This method will throw an error in 2.0, and will be removed in 2.1 -*/ -QUnit.reset = function() { - - // Return on non-browser environments - // This is necessary to not break on node tests - if ( typeof window === "undefined" ) { - return; - } - - var fixture = id( "qunit-fixture" ); - if ( fixture ) { - fixture.innerHTML = config.fixture; - } -}; - -// Don't load the HTML Reporter on non-Browser environments -if ( typeof window === "undefined" ) { - return; -} - -var config = QUnit.config, - hasOwn = Object.prototype.hasOwnProperty, - defined = { - document: typeof window.document !== "undefined", - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch ( e ) { - return false; - } - }()) - }; - -/** -* Escape text for attribute or text content. -*/ -function escapeText( s ) { - if ( !s ) { - return ""; - } - s = s + ""; - - // Both single quotes and double quotes (for attributes) - return s.replace( /['"<>&]/g, function( s ) { - switch ( s ) { - case "'": - return "'"; - case "\"": - return """; - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - } - }); -} - -/** - * @param {HTMLElement} elem - * @param {string} type - * @param {Function} fn - */ -function addEvent( elem, type, fn ) { - if ( elem.addEventListener ) { - - // Standards-based browsers - elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - - // support: IE <9 - elem.attachEvent( "on" + type, fn ); - } -} - -/** - * @param {Array|NodeList} elems - * @param {string} type - * @param {Function} fn - */ -function addEvents( elems, type, fn ) { - var i = elems.length; - while ( i-- ) { - addEvent( elems[ i ], type, fn ); - } -} - -function hasClass( elem, name ) { - return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; -} - -function addClass( elem, name ) { - if ( !hasClass( elem, name ) ) { - elem.className += ( elem.className ? " " : "" ) + name; - } -} - -function toggleClass( elem, name ) { - if ( hasClass( elem, name ) ) { - removeClass( elem, name ); - } else { - addClass( elem, name ); - } -} - -function removeClass( elem, name ) { - var set = " " + elem.className + " "; - - // Class name may appear multiple times - while ( set.indexOf( " " + name + " " ) >= 0 ) { - set = set.replace( " " + name + " ", " " ); - } - - // trim for prettiness - elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); -} - -function id( name ) { - return defined.document && document.getElementById && document.getElementById( name ); -} - -function getUrlConfigHtml() { - var i, j, val, - escaped, escapedTooltip, - selection = false, - len = config.urlConfig.length, - urlConfigHtml = ""; - - for ( i = 0; i < len; i++ ) { - val = config.urlConfig[ i ]; - if ( typeof val === "string" ) { - val = { - id: val, - label: val - }; - } - - escaped = escapeText( val.id ); - escapedTooltip = escapeText( val.tooltip ); - - config[ val.id ] = QUnit.urlParams[ val.id ]; - if ( !val.value || typeof val.value === "string" ) { - urlConfigHtml += ""; - } else { - urlConfigHtml += ""; - } - } - - return urlConfigHtml; -} - -function toolbarUrlConfigContainer() { - var urlConfigContainer = document.createElement( "span" ); - - urlConfigContainer.innerHTML = getUrlConfigHtml(); - - // For oldIE support: - // * Add handlers to the individual elements instead of the container - // * Use "click" instead of "change" for checkboxes - // * Fallback from event.target to event.srcElement - addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", function( event ) { - var params = {}, - target = event.target || event.srcElement; - params[ target.name ] = target.checked ? - target.defaultValue || true : - undefined; - window.location = QUnit.url( params ); - }); - addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", function( event ) { - var params = {}, - target = event.target || event.srcElement; - params[ target.name ] = target.options[ target.selectedIndex ].value || undefined; - window.location = QUnit.url( params ); - }); - - return urlConfigContainer; -} - -function getModuleNames() { - var i, - moduleNames = []; - - for ( i in config.modules ) { - if ( config.modules.hasOwnProperty( i ) ) { - moduleNames.push( i ); - } - } - - moduleNames.sort(function( a, b ) { - return a.localeCompare( b ); - }); - - return moduleNames; -} - -function toolbarModuleFilterHtml() { - var i, - moduleFilterHtml = "", - moduleNames = getModuleNames(); - - if ( moduleNames.length <= 1 ) { - return false; - } - - moduleFilterHtml += "" + - ""; - - return moduleFilterHtml; -} - -function toolbarModuleFilter() { - var moduleFilter = document.createElement( "span" ), - moduleFilterHtml = toolbarModuleFilterHtml(); - - if ( !moduleFilterHtml ) { - return false; - } - - moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); - moduleFilter.innerHTML = moduleFilterHtml; - - addEvent( moduleFilter.lastChild, "change", function() { - var selectBox = moduleFilter.getElementsByTagName( "select" )[ 0 ], - selectedModule = decodeURIComponent( selectBox.options[ selectBox.selectedIndex ].value ); - - window.location = QUnit.url({ - module: ( selectedModule === "" ) ? undefined : selectedModule, - - // Remove any existing filters - filter: undefined, - testNumber: undefined - }); - }); - - return moduleFilter; -} - -function toolbarFilter() { - var testList = id( "qunit-tests" ), - filter = document.createElement( "input" ); - - filter.type = "checkbox"; - filter.id = "qunit-filter-pass"; - - addEvent( filter, "click", function() { - if ( filter.checked ) { - addClass( testList, "hidepass" ); - if ( defined.sessionStorage ) { - sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); - } - } else { - removeClass( testList, "hidepass" ); - if ( defined.sessionStorage ) { - sessionStorage.removeItem( "qunit-filter-passed-tests" ); - } - } - }); - - if ( config.hidepassed || defined.sessionStorage && - sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { - filter.checked = true; - - addClass( testList, "hidepass" ); - } - - return filter; -} - -function toolbarLabel() { - var label = document.createElement( "label" ); - label.setAttribute( "for", "qunit-filter-pass" ); - label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); - label.innerHTML = "Hide passed tests"; - - return label; -} - -function appendToolbar() { - var moduleFilter, - toolbar = id( "qunit-testrunner-toolbar" ); - - if ( toolbar ) { - toolbar.appendChild( toolbarFilter() ); - toolbar.appendChild( toolbarLabel() ); - toolbar.appendChild( toolbarUrlConfigContainer() ); - - moduleFilter = toolbarModuleFilter(); - if ( moduleFilter ) { - toolbar.appendChild( moduleFilter ); - } - } -} - -function appendBanner() { - var banner = id( "qunit-banner" ); - - if ( banner ) { - banner.className = ""; - banner.innerHTML = "" + banner.innerHTML + " "; - } -} - -function appendTestResults() { - var tests = id( "qunit-tests" ), - result = id( "qunit-testresult" ); - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - tests.innerHTML = ""; - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = "Running...
     "; - } -} - -function storeFixture() { - var fixture = id( "qunit-fixture" ); - if ( fixture ) { - config.fixture = fixture.innerHTML; - } -} - -function appendUserAgent() { - var userAgent = id( "qunit-userAgent" ); - if ( userAgent ) { - userAgent.innerHTML = navigator.userAgent; - } -} - -// HTML Reporter initialization and load -QUnit.begin(function() { - var qunit = id( "qunit" ); - - if ( qunit ) { - qunit.innerHTML = - "

    " + escapeText( document.title ) + "

    " + - "

    " + - "
    " + - "

    " + - "
      "; - } - - appendBanner(); - appendTestResults(); - appendUserAgent(); - appendToolbar(); - storeFixture(); -}); - -QUnit.done(function( details ) { - var i, key, - banner = id( "qunit-banner" ), - tests = id( "qunit-tests" ), - html = [ - "Tests completed in ", - details.runtime, - " milliseconds.
      ", - "", - details.passed, - " assertions of ", - details.total, - " passed, ", - details.failed, - " failed." - ].join( "" ); - - if ( banner ) { - banner.className = details.failed ? "qunit-fail" : "qunit-pass"; - } - - if ( tests ) { - id( "qunit-testresult" ).innerHTML = html; - } - - if ( config.altertitle && defined.document && document.title ) { - - // show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8-charset - document.title = [ - ( details.failed ? "\u2716" : "\u2714" ), - document.title.replace( /^[\u2714\u2716] /i, "" ) - ].join( " " ); - } - - // clear own sessionStorage items if all tests passed - if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { - for ( i = 0; i < sessionStorage.length; i++ ) { - key = sessionStorage.key( i++ ); - if ( key.indexOf( "qunit-test-" ) === 0 ) { - sessionStorage.removeItem( key ); - } - } - } - - // scroll back to top to show results - if ( config.scrolltop && window.scrollTo ) { - window.scrollTo( 0, 0 ); - } -}); - -function getNameHtml( name, module ) { - var nameHtml = ""; - - if ( module ) { - nameHtml = "" + escapeText( module ) + ": "; - } - - nameHtml += "" + escapeText( name ) + ""; - - return nameHtml; -} - -QUnit.testStart(function( details ) { - var a, b, li, running, assertList, - name = getNameHtml( details.name, details.module ), - tests = id( "qunit-tests" ); - - if ( tests ) { - b = document.createElement( "strong" ); - b.innerHTML = name; - - a = document.createElement( "a" ); - a.innerHTML = "Rerun"; - a.href = QUnit.url({ testNumber: details.testNumber }); - - li = document.createElement( "li" ); - li.appendChild( b ); - li.appendChild( a ); - li.className = "running"; - li.id = "qunit-test-output" + details.testNumber; - - assertList = document.createElement( "ol" ); - assertList.className = "qunit-assert-list"; - - li.appendChild( assertList ); - - tests.appendChild( li ); - } - - running = id( "qunit-testresult" ); - if ( running ) { - running.innerHTML = "Running:
      " + name; - } - -}); - -QUnit.log(function( details ) { - var assertList, assertLi, - message, expected, actual, - testItem = id( "qunit-test-output" + details.testNumber ); - - if ( !testItem ) { - return; - } - - message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); - message = "" + message + ""; - - // pushFailure doesn't provide details.expected - // when it calls, it's implicit to also not show expected and diff stuff - // Also, we need to check details.expected existence, as it can exist and be undefined - if ( !details.result && hasOwn.call( details, "expected" ) ) { - expected = escapeText( QUnit.dump.parse( details.expected ) ); - actual = escapeText( QUnit.dump.parse( details.actual ) ); - message += ""; - - if ( actual !== expected ) { - message += "" + - ""; - } - - if ( details.source ) { - message += ""; - } - - message += "
      Expected:
      " +
      -			expected +
      -			"
      Result:
      " +
      -				actual + "
      Diff:
      " +
      -				QUnit.diff( expected, actual ) + "
      Source:
      " +
      -				escapeText( details.source ) + "
      "; - - // this occours when pushFailure is set and we have an extracted stack trace - } else if ( !details.result && details.source ) { - message += "" + - "" + - "
      Source:
      " +
      -			escapeText( details.source ) + "
      "; - } - - assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; - - assertLi = document.createElement( "li" ); - assertLi.className = details.result ? "pass" : "fail"; - assertLi.innerHTML = message; - assertList.appendChild( assertLi ); -}); - -QUnit.testDone(function( details ) { - var testTitle, time, testItem, assertList, - good, bad, testCounts, - tests = id( "qunit-tests" ); - - // QUnit.reset() is deprecated and will be replaced for a new - // fixture reset function on QUnit 2.0/2.1. - // It's still called here for backwards compatibility handling - QUnit.reset(); - - if ( !tests ) { - return; - } - - testItem = id( "qunit-test-output" + details.testNumber ); - assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; - - good = details.passed; - bad = details.failed; - - // store result when possible - if ( config.reorder && defined.sessionStorage ) { - if ( bad ) { - sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); - } else { - sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); - } - } - - if ( bad === 0 ) { - addClass( assertList, "qunit-collapsed" ); - } - - // testItem.firstChild is the test name - testTitle = testItem.firstChild; - - testCounts = bad ? - "" + bad + ", " + "" + good + ", " : - ""; - - testTitle.innerHTML += " (" + testCounts + - details.assertions.length + ")"; - - addEvent( testTitle, "click", function() { - toggleClass( assertList, "qunit-collapsed" ); - }); - - time = document.createElement( "span" ); - time.className = "runtime"; - time.innerHTML = details.runtime + " ms"; - - testItem.className = bad ? "fail" : "pass"; - - testItem.insertBefore( time, assertList ); -}); - -if ( !defined.document || document.readyState === "complete" ) { - config.autorun = true; -} - -if ( defined.document ) { - addEvent( window, "load", QUnit.load ); -} - -})(); diff --git a/test/test/wasm_exec.js b/test/test/wasm_exec.js new file mode 100644 index 0000000..94b9552 --- /dev/null +++ b/test/test/wasm_exec.js @@ -0,0 +1,437 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +(() => { + // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API). + const isNodeJS = typeof process !== "undefined"; + if (isNodeJS) { + global.require = require; + global.fs = require("fs"); + + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + + const util = require("util"); + global.TextEncoder = util.TextEncoder; + global.TextDecoder = util.TextDecoder; + } else { + if (typeof window !== "undefined") { + window.global = window; + } else if (typeof self !== "undefined") { + self.global = self; + } else { + throw new Error("cannot export Go (neither window nor self is defined)"); + } + + let outputBuf = ""; + global.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + openSync(path, flags, mode) { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + throw err; + }, + }; + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + global.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.mem.buffer); + } + + const setInt64 = (addr, v) => { + mem().setUint32(addr + 0, v, true); + mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const getInt64 = (addr) => { + const low = mem().getUint32(addr + 0, true); + const high = mem().getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = mem().getFloat64(addr, true); + if (!isNaN(f)) { + return f; + } + + const id = mem().getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number") { + if (isNaN(v)) { + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 0, true); + return; + } + mem().setFloat64(addr, v, true); + return; + } + + switch (v) { + case undefined: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 1, true); + return; + case null: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 2, true); + return; + case true: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 3, true); + return; + case false: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 4, true); + return; + } + + let ref = this._refs.get(v); + if (ref === undefined) { + ref = this._values.length; + this._values.push(v); + this._refs.set(v, ref); + } + let typeFlag = 0; + switch (typeof v) { + case "string": + typeFlag = 1; + break; + case "symbol": + typeFlag = 2; + break; + case "function": + typeFlag = 3; + break; + } + mem().setUint32(addr + 4, nanHead | typeFlag, true); + mem().setUint32(addr, ref, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + go: { + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + const code = mem().getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._refs; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = mem().getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func nanotime() int64 + "runtime.nanotime": (sp) => { + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleCallback(delay int64) int32 + "runtime.scheduleCallback": (sp) => { + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._callbackTimeouts.set(id, setTimeout( + () => { this._resolveCallbackPromise(); }, + getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early + )); + mem().setInt32(sp + 16, id, true); + }, + + // func clearScheduledCallback(id int32) + "runtime.clearScheduledCallback": (sp) => { + const id = mem().getInt32(sp + 8, true); + clearTimeout(this._callbackTimeouts.get(id)); + this._callbackTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + storeValue(sp + 32, Reflect.get(loadValue(sp + 8), loadString(sp + 16))); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + storeValue(sp + 56, Reflect.apply(m, v, args)); + mem().setUint8(sp + 64, 1); + } catch (err) { + storeValue(sp + 56, err); + mem().setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + storeValue(sp + 40, Reflect.apply(v, undefined, args)); + mem().setUint8(sp + 48, 1); + } catch (err) { + storeValue(sp + 40, err); + mem().setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + storeValue(sp + 40, Reflect.construct(v, args)); + mem().setUint8(sp + 48, 1); + } catch (err) { + storeValue(sp + 40, err); + mem().setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + this._inst = instance; + this._values = [ // TODO: garbage collection + NaN, + undefined, + null, + true, + false, + global, + this._inst.exports.mem, + this, + ]; + this._refs = new Map(); + this._callbackShutdown = false; + this.exited = false; + + const mem = new DataView(this._inst.exports.mem.buffer) + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + let ptr = offset; + new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); + offset += str.length + (8 - (str.length % 8)); + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + + const keys = Object.keys(this.env).sort(); + argvPtrs.push(keys.length); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + + const argv = offset; + argvPtrs.forEach((ptr) => { + mem.setUint32(offset, ptr, true); + mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + while (true) { + const callbackPromise = new Promise((resolve) => { + this._resolveCallbackPromise = () => { + if (this.exited) { + throw new Error("bad callback: Go program has already exited"); + } + setTimeout(resolve, 0); // make sure it is asynchronous + }; + }); + this._inst.exports.run(argc, argv); + if (this.exited) { + break; + } + await callbackPromise; + } + } + + static _makeCallbackHelper(id, pendingCallbacks, go) { + return function() { + pendingCallbacks.push({ id: id, args: arguments }); + go._resolveCallbackPromise(); + }; + } + + static _makeEventCallbackHelper(preventDefault, stopPropagation, stopImmediatePropagation, fn) { + return function(event) { + if (preventDefault) { + event.preventDefault(); + } + if (stopPropagation) { + event.stopPropagation(); + } + if (stopImmediatePropagation) { + event.stopImmediatePropagation(); + } + fn(event); + }; + } + } + + if (isNodeJS) { + if (process.argv.length < 3) { + process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n"); + process.exit(1); + } + + const go = new Go(); + go.argv = process.argv.slice(2); + go.env = process.env; + go.exit = process.exit; + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + process.on("exit", (code) => { // Node.js exits if no callback is pending + if (code === 0 && !go.exited) { + // deadlock, make Go print error and stack traces + go._callbackShutdown = true; + go._inst.exports.run(); + } + }); + return go.run(result.instance); + }).catch((err) => { + throw err; + }); + } +})(); diff --git a/test/test/websocket_test.go b/test/test/websocket_test.go new file mode 100644 index 0000000..7200cfd --- /dev/null +++ b/test/test/websocket_test.go @@ -0,0 +1,247 @@ +package websocket_test + +import ( + "bytes" + "crypto/rand" + "io" + "testing" + "time" + + "github.com/LinearZoetrope/testevents" + "github.com/gopherjs/websocket" +) + +func TestConnImmediateClose(t_ *testing.T) { + t := testevents.Start(t_, "TestConnImmediateClose", true) + defer t.Done() + + ws, err := websocket.Dial(getWSBaseURL() + "immediate-close") + if err != nil { + t.Fatalf("Error opening WebSocket: %s", err) + } + defer ws.Close() + + t.Log("WebSocket opened") + + _, err = ws.Read(nil) + if err == io.EOF { + t.Log("Received EOF") + } else if err != nil { + t.Fatalf("Unexpected error in second read: %s", err) + } else { + t.Fatalf("Expected EOF in second read, got no error") + } +} + +func TestConnFailedOpen(t_ *testing.T) { + t := testevents.Start(t_, "TestConnFailedOpen", true) + defer t.Done() + + ws, err := websocket.Dial(getWSBaseURL() + "404-not-found") + if err == nil { + ws.Close() + t.Fatalf("Got no error, but expected an error in opening the WebSocket.") + } + + t.Logf("WebSocket failed to open: %s", err) +} + +func TestConnBinaryRead(t_ *testing.T) { + t := testevents.Start(t_, "TestConnBinaryRead", true) + defer t.Done() + + ws, err := websocket.Dial(getWSBaseURL() + "binary-static") + if err != nil { + t.Fatalf("Error opening WebSocket: %s", err) + } + defer ws.Close() + + t.Logf("WebSocket opened") + + var expectedData = []byte{0x00, 0x01, 0x02, 0x03, 0x04} + + receivedData := make([]byte, len(expectedData)) + n, err := ws.Read(receivedData) + if err != nil { + t.Fatalf("Error in first read: %s", err) + return + } + receivedData = receivedData[:n] + + if !bytes.Equal(receivedData, expectedData) { + t.Fatalf("Received data did not match expected data. Got % x, expected % x.", receivedData, expectedData) + } else { + t.Logf("Received data: % x", receivedData) + } + + _, err = ws.Read(receivedData) + if err == io.EOF { + t.Logf("Received EOF") + } else if err != nil { + t.Fatalf("Unexpected error in second read: %s", err) + } else { + t.Fatalf("Expected EOF in second read, got no error") + } +} + +func TestConnBinaryEcho(t_ *testing.T) { + t := testevents.Start(t_, "TestConnBinaryEcho", true) + defer t.Done() + + data := make([]byte, 1024*1024) + + totalN := 0 + for totalN < len(data) { + sliceEnd := totalN + 65535 + if sliceEnd > len(data) { + sliceEnd = len(data) + } + n, err := rand.Read(data[totalN:sliceEnd]) + if err != nil { + t.Fatalf("Error in creating data: %s", err) + } + totalN = totalN + n + } + + data = data[:totalN] + + t.Logf("Created %d bytes to send", len(data)) + + ws, err := websocket.Dial(getWSBaseURL() + "echo") + if err != nil { + t.Fatalf("Error opening WebSocket: %s", err) + } + defer ws.Close() + + t.Logf("WebSocket opened") + + byteReader := bytes.NewReader(data) + nSent, err := io.Copy(ws, byteReader) + if err != nil { + t.Fatalf("Error sending data: %s", err) + } + + t.Logf("Sent %d bytes", nSent) + + receivedData := make([]byte, len(data)) + n, err := io.ReadAtLeast(ws, receivedData, len(receivedData)) + if err != nil { + t.Fatalf("Error in read: %s", err) + } + receivedData = receivedData[:n] + + t.Logf("Received %d bytes", n) + + if !bytes.Equal(receivedData, data) { + t.Fatalf("Received data did not match expected data.") + } else { + t.Logf("Received correct data") + } + + receivedData = receivedData[:256] + + // Check for extra data + ws.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) + n, err = ws.Read(receivedData) + if n != 0 { + t.Fatalf("Extra data was received") + } else if err != nil && err.Error() == "i/o timeout: deadline reached" { + t.Logf("No extra data received") + } else if err != nil { + t.Fatalf("Error checking for extra data: %s", err) + } +} + +func TestConnMultiFrameRead(t_ *testing.T) { + t := testevents.Start(t_, "TestConnMultiFrameRead", true) + defer t.Done() + + ws, err := websocket.Dial(getWSBaseURL() + "multiframe-static") + if err != nil { + t.Fatalf("Error opening WebSocket: %s", err) + } + defer ws.Close() + + t.Logf("WebSocket opened") + + var expectedData = []byte{0x00, 0x01, 0x02, 0x03, 0x04} + + receivedData := make([]byte, len(expectedData)) + n, err := io.ReadAtLeast(ws, receivedData, len(expectedData)) + if err != nil { + t.Fatalf("Error in read: %s", err) + return + } + receivedData = receivedData[:n] + + if !bytes.Equal(receivedData, expectedData) { + t.Fatalf("Received data did not match expected data. Got % x, expected % x.", receivedData, expectedData) + } else { + t.Logf("Received data: % x", receivedData) + } + + _, err = ws.Read(receivedData) + if err == io.EOF { + t.Logf("Received EOF") + } else if err != nil { + t.Fatalf("Unexpected error in second read: %s", err) + } else { + t.Fatalf("Expected EOF in second read, got no error") + } +} + +func TestConn1MBRead(t_ *testing.T) { + t := testevents.Start(t_, "TestConn1MBRead", true) + defer t.Done() + + ws, err := websocket.Dial(getWSBaseURL() + "random-1mb") + if err != nil { + t.Fatalf("Error opening WebSocket: %s", err) + } + defer ws.Close() + + bytesRead := 0 + data := make([]byte, 1024) + for i := 0; i < 1024; i++ { + n, err := io.ReadAtLeast(ws, data, len(data)) + if err != nil { + t.Fatalf("Error reading 1024 bytes: %s", err) + } + bytesRead = bytesRead + n + } + + if bytesRead != 1024*1024 { + t.Fatalf("Read %d bytes; expected %d bytes", bytesRead, 1024*1024) + } + t.Logf("%d bytes successfuly read", bytesRead) +} + +func TestConnTimeout(t_ *testing.T) { + t := testevents.Start(t_, "TestWSTimeout", true) + defer t.Done() + + ws, err := websocket.Dial(getWSBaseURL() + "wait-30s") + if err != nil { + t.Fatalf("Error opening WebSocket: %s", err) + } + defer ws.Close() + + t.Logf("WebSocket opened") + + start := time.Now() + timeoutTime := time.Now().Add(1 * time.Second) + ws.SetReadDeadline(timeoutTime) + + _, err = ws.Read(nil) + if err != nil && err.Error() == "i/o timeout: deadline reached" { + totalTime := time.Now().Sub(start) + if time.Now().Before(timeoutTime) { + t.Fatalf("Timeout was too short: Received timeout after %s", totalTime) + } + t.Logf("Received timeout after %s", totalTime) + } else if err != nil { + t.Fatalf("Unexpected error in read: %s", err) + } else { + t.Fatalf("Expected timeout in read, got no error") + } +} diff --git a/test/test/websocketjs_test.go b/test/test/websocketjs_test.go new file mode 100644 index 0000000..d66f999 --- /dev/null +++ b/test/test/websocketjs_test.go @@ -0,0 +1,69 @@ +package websocket_test + +import ( + "sync" + "syscall/js" + "testing" + + "github.com/LinearZoetrope/testevents" + "github.com/gopherjs/websocket/websocketjs" +) + +func TestWSInvalidURL(t_ *testing.T) { + t := testevents.Start(t_, "TestWSInvalidURL", true) + defer t.Done() + + ws, err := websocketjs.New("blah://blah.example/invalid") + if err == nil { + ws.Close() + t.Fatalf("Got no error, but expected an invalid URL error") + } +} + +func TestWSImmediateClose(t_ *testing.T) { + t := testevents.Start(t_, "TestWSImmediateClose", true) + defer t.Done() + + ws, err := websocketjs.New(getWSBaseURL() + "immediate-close") + if err != nil { + t.Fatalf("Error opening WebSocket: %s", err) + } + defer ws.Close() + + var wg sync.WaitGroup + + var ( + openCallback js.Callback + closeCallback js.Callback + ) + + openCallback = js.NewEventCallback(0, func(ev js.Value) { + defer ws.RemoveEventListener("open", openCallback) + + t.Logf("WebSocket opened") + }) + defer openCallback.Release() + ws.AddEventListener("open", openCallback) + + closeCallback = js.NewEventCallback(0, func(ev js.Value) { + defer wg.Done() + defer ws.RemoveEventListener("close", closeCallback) + + const ( + CloseNormalClosure = 1000 + CloseNoStatusReceived = 1005 // IE10 hates it when the server closes without sending a close reason + ) + + closeEventCode := ev.Get("code").Int() + + if closeEventCode != CloseNormalClosure && closeEventCode != CloseNoStatusReceived { + t.Fatalf("WebSocket close was not clean (code %d)", closeEventCode) + } + t.Logf("WebSocket closed") + }) + defer closeCallback.Release() + ws.AddEventListener("close", closeCallback) + wg.Add(1) + + wg.Wait() +} diff --git a/websocketjs/websocketjs.go b/websocketjs/websocketjs.go index 68acfcb..f0920f7 100644 --- a/websocketjs/websocketjs.go +++ b/websocketjs/websocketjs.go @@ -30,7 +30,7 @@ such as adding event listeners with callbacks. */ package websocketjs -import "github.com/gopherjs/gopherjs/js" +import "github.com/gopherjs/gopherwasm/js" // ReadyState represents the state that a WebSocket is in. For more information // about the available states, see @@ -73,7 +73,7 @@ func New(url string) (ws *WebSocket, err error) { if e == nil { return } - if jsErr, ok := e.(*js.Error); ok && jsErr != nil { + if jsErr, ok := e.(js.Error); ok { ws = nil err = jsErr } else { @@ -81,10 +81,10 @@ func New(url string) (ws *WebSocket, err error) { } }() - object := js.Global.Get("WebSocket").New(url) + webSocket := js.Global().Get("WebSocket").New(url) ws = &WebSocket{ - Object: object, + Value: webSocket, } return @@ -94,42 +94,29 @@ func New(url string) (ws *WebSocket, err error) { // object. For more information, see // http://dev.w3.org/html5/websockets/#the-websocket-interface type WebSocket struct { - *js.Object - - URL string `js:"url"` - - // ready state - ReadyState ReadyState `js:"readyState"` - BufferedAmount uint32 `js:"bufferedAmount"` - - // networking - Extensions string `js:"extensions"` - Protocol string `js:"protocol"` - - // messaging - BinaryType string `js:"binaryType"` + js.Value } // AddEventListener provides the ability to bind callback // functions to the following available events: // open, error, close, message -func (ws *WebSocket) AddEventListener(typ string, useCapture bool, listener func(*js.Object)) { - ws.Call("addEventListener", typ, listener, useCapture) +func (ws *WebSocket) AddEventListener(typ string, callback js.Callback) { + ws.Call("addEventListener", typ, callback) } // RemoveEventListener removes a previously bound callback function -func (ws *WebSocket) RemoveEventListener(typ string, useCapture bool, listener func(*js.Object)) { - ws.Call("removeEventListener", typ, listener, useCapture) +func (ws *WebSocket) RemoveEventListener(typ string, callback js.Callback) { + ws.Call("removeEventListener", typ, callback) } // BUG(nightexcessive): When WebSocket.Send is called on a closed WebSocket, the // thrown error doesn't seem to be caught by recover. // Send sends a message on the WebSocket. The data argument can be a string or a -// *js.Object fulfilling the ArrayBufferView definition. +// js.Value fulfilling the ArrayBufferView definition. // // See: http://dev.w3.org/html5/websockets/#dom-websocket-send -func (ws *WebSocket) Send(data interface{}) (err error) { +func (ws *WebSocket) Send(data js.Value) (err error) { defer func() { e := recover() if e == nil { @@ -141,7 +128,7 @@ func (ws *WebSocket) Send(data interface{}) (err error) { panic(e) } }() - ws.Object.Call("send", data) + ws.Value.Call("send", data) return } @@ -164,7 +151,7 @@ func (ws *WebSocket) Close() (err error) { // Use close code closeNormalClosure to indicate that the purpose // for which the connection was established has been fulfilled. // See https://tools.ietf.org/html/rfc6455#section-7.4. - ws.Object.Call("close", closeNormalClosure) + ws.Value.Call("close", closeNormalClosure) return }