diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ffb39f0..edf6a28b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,12 +29,14 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - scala: [3.1.1, 2.13.8] + scala: [3.1.2, 2.13.8] java: [temurin@8] - browser: [Chrome, Firefox] + jsenv: [Chrome, Firefox, NodeJS] exclude: - - scala: 3.1.1 - browser: Firefox + - scala: 3.1.2 + jsenv: Firefox + - scala: 3.1.2 + jsenv: NodeJS runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) @@ -70,41 +72,47 @@ jobs: ~/Library/Caches/Coursier/v1 key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + - name: Setup NodeJS v18 + if: matrix.jsenv == 'NodeJS' + uses: actions/setup-node@v2 + with: + node-version: 18 + - name: Check that workflows are up to date - run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.browser }}'' ''project /'' githubWorkflowCheck' + run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' ''project /'' githubWorkflowCheck' - name: Check headers and formatting if: matrix.java == 'temurin@8' - run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.browser }}'' headerCheckAll scalafmtCheckAll ''project /'' scalafmtSbtCheck' + run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' headerCheckAll scalafmtCheckAll ''project /'' scalafmtSbtCheck' - name: Test - run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.browser }}'' test' + run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' test' - name: Check binary compatibility if: matrix.java == 'temurin@8' - run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.browser }}'' mimaReportBinaryIssues' + run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' mimaReportBinaryIssues' - name: Generate API documentation if: matrix.java == 'temurin@8' - run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.browser }}'' doc' + run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' doc' - name: Check unused compile dependencies if: matrix.java == 'temurin@8' - run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.browser }}'' unusedCompileDependenciesTest' + run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' unusedCompileDependenciesTest' - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p target site/target tests/target dom/target jsdocs/target project/target + run: mkdir -p target site/target tests/target dom/target tests-nodejs/target jsdocs/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar target site/target tests/target dom/target jsdocs/target project/target + run: tar cf targets.tar target site/target tests/target dom/target tests-nodejs/target jsdocs/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') uses: actions/upload-artifact@v2 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.browser }} + name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.jsenv }} path: targets.tar publish: @@ -151,12 +159,12 @@ jobs: ~/Library/Caches/Coursier/v1 key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - - name: Download target directories (3.1.1, Chrome) + - name: Download target directories (3.1.2, Chrome) uses: actions/download-artifact@v2 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.1.1-Chrome + name: target-${{ matrix.os }}-${{ matrix.java }}-3.1.2-Chrome - - name: Inflate target directories (3.1.1, Chrome) + - name: Inflate target directories (3.1.2, Chrome) run: | tar xf targets.tar rm targets.tar diff --git a/.scalafmt.conf b/.scalafmt.conf index 2563ef7c..7e57088d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.4.3 +version = 3.5.2 runner.dialect = Scala213Source3 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 43ccabb3..712cf47d 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -6,7 +6,7 @@ Everyone is expected to follow the [Scala Code of Conduct] when discussing the p ## Moderation -For any questions, concerns, or moderation requests, please contact a member of the [community staff](https://http4s.org/code-of-conduct/#moderation) +For any questions, concerns, or moderation requests, please contact a member of the [community staff](https://http4s.org/code-of-conduct.html#moderation) -[Scala Code of Conduct]: https://http4s.org/code-of-conduct/ -[Community staff]: https://http4s.org/code-of-conduct/#moderation +[Scala Code of Conduct]: https://http4s.org/code-of-conduct.html +[Community staff]: https://http4s.org/code-of-conduct.html#moderation diff --git a/README.md b/README.md index 281a2437..a1ba0993 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,12 @@ Features: Notably, http4s-dom can also be used to create _serverless_ apps with [Cloudflare Workers](https://workers.cloudflare.com) which have adopted the same APIs used in the browser! +It is also possible to use the `FetchClient` in Node.js v18+, which added [experimental support](https://nodejs.org/en/blog/announcements/v18-release-announce/#fetch-experimental) for `fetch`, although some browser-specific features may not be available. + ### Usage [![http4s-dom Scala version support](https://index.scala-lang.org/http4s/http4s-dom/http4s-dom/latest.svg)](https://index.scala-lang.org/http4s/http4s-dom/http4s-dom) ```scala -// Supports http4s 0.23.x -libraryDependencies += "org.http4s" %%% "http4s-dom" % "0.2.1" +libraryDependencies += "org.http4s" %%% "http4s-dom" % "0.2.2" ``` diff --git a/build.sbt b/build.sbt index c9f7c079..3884514d 100644 --- a/build.sbt +++ b/build.sbt @@ -17,6 +17,7 @@ import org.openqa.selenium.WebDriver import org.openqa.selenium.chrome.ChromeOptions import org.openqa.selenium.firefox.FirefoxOptions +import org.scalajs.jsenv.nodejs.NodeJSEnv import org.scalajs.jsenv.selenium.SeleniumJSEnv import JSEnv._ @@ -28,24 +29,33 @@ ThisBuild / developers := List( tlGitHubDev("armanbilge", "Arman Bilge") ) ThisBuild / startYear := Some(2021) -ThisBuild / tlSiteApiUrl := Some(url( - "https://www.javadoc.io/doc/org.http4s/http4s-dom_sjs1_2.13/latest/org/http4s/dom/index.html")) ThisBuild / githubWorkflowTargetBranches := Seq("main") ThisBuild / tlCiReleaseBranches := Seq("main") ThisBuild / tlSitePublishBranch := Some("series/0.2") -ThisBuild / crossScalaVersions := Seq("3.1.1", "2.13.8") +ThisBuild / crossScalaVersions := Seq("3.1.2", "2.13.8") ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("8")) -ThisBuild / githubWorkflowBuildMatrixAdditions += "browser" -> List("Chrome", "Firefox") -ThisBuild / githubWorkflowBuildSbtStepPreamble += s"set Global / useJSEnv := JSEnv.$${{ matrix.browser }}" + +val jsEnvs = List("Chrome", "Firefox", "NodeJS") +ThisBuild / githubWorkflowBuildMatrixAdditions += "jsenv" -> jsEnvs +ThisBuild / githubWorkflowBuildSbtStepPreamble += s"set Global / useJSEnv := JSEnv.$${{ matrix.jsenv }}" ThisBuild / githubWorkflowBuildMatrixExclusions ++= { for { scala <- (ThisBuild / crossScalaVersions).value.init - } yield MatrixExclude(Map("scala" -> scala, "browser" -> "Firefox")) + jsenv <- jsEnvs.tail + } yield MatrixExclude(Map("scala" -> scala, "jsenv" -> jsenv)) } -lazy val useJSEnv = settingKey[JSEnv]("Browser for running Scala.js tests") +ThisBuild / githubWorkflowBuildPreamble += + WorkflowStep.Use( + UseRef.Public("actions", "setup-node", "v2"), + name = Some("Setup NodeJS v18"), + params = Map("node-version" -> "18"), + cond = Some("matrix.jsenv == 'NodeJS'") + ) + +lazy val useJSEnv = settingKey[JSEnv]("JSEnv for running Scala.js tests") Global / useJSEnv := Chrome lazy val fileServicePort = settingKey[Int]("Port for static file server") @@ -98,6 +108,7 @@ ThisBuild / Test / jsEnv := { s"http://localhost:${fileServicePort.value}/target/selenium/") useJSEnv.value match { + case NodeJS => new NodeJSEnv() case Chrome => val options = new ChromeOptions() options.setHeadless(true) @@ -109,16 +120,16 @@ ThisBuild / Test / jsEnv := { } } -val catsEffectVersion = "3.3.8" -val fs2Version = "3.2.5" -val http4sVersion = "1.0.0-M32" -val scalaJSDomVersion = "2.1.0" -val circeVersion = "0.15.0-M1" +val catsEffectVersion = "3.3.12" +val fs2Version = "3.2.7" +val http4sVersion = "1.0.0-M33" +val scalaJSDomVersion = "2.2.0" +val circeVersion = "0.14.2" val munitVersion = "0.7.29" val munitCEVersion = "1.0.7" lazy val root = - project.in(file(".")).aggregate(dom, tests).enablePlugins(NoPublishPlugin) + project.in(file(".")).aggregate(dom, tests, nodeJSTests).enablePlugins(NoPublishPlugin) lazy val dom = project .in(file("dom")) @@ -138,10 +149,25 @@ lazy val tests = project .in(file("tests")) .settings( scalaJSUseMainModuleInitializer := true, - (Test / test) := (Test / test).dependsOn(Compile / fastOptJS).value, - buildInfoKeys := Seq[BuildInfoKey](scalaVersion, fileServicePort), + Test / test := { + if (useJSEnv.value != NodeJS) + (Test / test).dependsOn(Compile / fastOptJS).value + else + () + }, + buildInfoKeys := Seq[BuildInfoKey]( + fileServicePort, + BuildInfoKey( + "workerDir" -> (Compile / fastLinkJS / scalaJSLinkerOutputDirectory) + .value + .relativeTo((ThisBuild / baseDirectory).value) + .get + .toString + ) + ), buildInfoPackage := "org.http4s.dom", libraryDependencies ++= Seq( + "org.http4s" %%% "http4s-client-testkit" % http4sVersion, "org.scalameta" %%% "munit" % munitVersion % Test, "org.typelevel" %%% "munit-cats-effect-3" % munitCEVersion % Test ) @@ -149,6 +175,25 @@ lazy val tests = project .dependsOn(dom) .enablePlugins(ScalaJSPlugin, BuildInfoPlugin, NoPublishPlugin) +lazy val nodeJSTests = project + .in(file("tests-nodejs")) + .settings( + Test / test := { + if (useJSEnv.value == NodeJS) + (Test / test).value + else + () + }, + libraryDependencies ++= Seq( + "org.http4s" %%% "http4s-client-testkit" % http4sVersion, + "org.scalameta" %%% "munit" % munitVersion % Test, + "org.typelevel" %%% "munit-cats-effect-3" % munitCEVersion % Test + ), + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)) + ) + .dependsOn(dom) + .enablePlugins(ScalaJSPlugin, NoPublishPlugin) + lazy val jsdocs = project .dependsOn(dom) @@ -164,14 +209,33 @@ lazy val jsdocs = lazy val docs = project .in(file("site")) .settings( + tlSiteApiModule := Some((dom / projectID).value), + tlSiteApiPackage := Some("org.http4s.dom"), tlFatalWarningsInCi := false, mdocJS := Some(jsdocs), + tlSiteRelatedProjects ++= Seq( + "calico" -> url("https://armanbilge.github.io/calico/") + ), mdocVariables ++= Map( - "js-opt" -> "fast", "HTTP4S_VERSION" -> http4sVersion, "CIRCE_VERSION" -> circeVersion ), - laikaConfig ~= { _.withRawContent }, + laikaConfig := { + import laika.rewrite.link._ + laikaConfig + .value + .withRawContent + .withConfigValue(LinkConfig(apiLinks = List( + ApiLinks( + baseUri = + s"https://www.javadoc.io/doc/org.http4s/http4s-dom_sjs1_2.13/${mdocVariables.value("VERSION")}/", + packagePrefix = "org.http4s.dom"), + ApiLinks( + baseUri = s"https://www.javadoc.io/doc/org.http4s/http4s-docs_2.13/$http4sVersion/", + packagePrefix = "org.http4s" + ) + ))) + }, tlSiteHeliumConfig ~= { // Actually, this *disables* auto-linking, to avoid duplicates with mdoc _.site.autoLinkJS() diff --git a/docs/fetch.md b/docs/fetch.md index dce39f75..cb7e86c8 100644 --- a/docs/fetch.md +++ b/docs/fetch.md @@ -1,6 +1,6 @@ # Fetch Client -The [`FetchClientBuilder`](https://www.javadoc.io/doc/org.http4s/http4s-dom_sjs1_2.13/latest/org/http4s/dom/FetchClientBuilder.html) creates a standard http4s [`Client`](https://http4s.org/v0.23/api/org/http4s/client/client) that is described in the [http4s documentation](https://http4s.org/v0.23/client/). +The @:api(org.http4s.dom.FetchClientBuilder) creates a standard http4s @:api(org.http4s.client.Client) that is described in the [http4s documentation](https://http4s.org/v0.23/docs/client.html). ## Example @@ -32,7 +32,7 @@ val fetchActivity: IO[Unit] = for { _ <- IO(activityElement.innerHTML = activity.activity) } yield () -val button = document.getElementById("button").asInstanceOf[html.Button] +val button = document.getElementById("button").asInstanceOf[HTMLButtonElement] button.onclick = _ => fetchActivity.unsafeRunAndForget() ``` diff --git a/docs/index.md b/docs/index.md index 9d4596e8..354adfca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,18 +4,19 @@ Use http4s in your browser with Scala.js! Features: * A [`Client` implementation](fetch.md) backed by [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) -* A [`WSClient` implementation](websocket.md) backed by [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket$.html) -* A [`Service Worker` integration](serviceworker.md) to install your `HttpRoutes` as a [`FetchEvent` handler](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/onfetch) +* A [`WSClient` implementation](websocket.md) backed by [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) +* A [`Service Worker` integration](serviceworker.md) to install your `HttpRoutes` as a [`FetchEvent` handler](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/fetch_event) * Encoders for [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File), [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) Notably, http4s-dom can also be used to create _serverless_ apps with [Cloudflare Workers](https://workers.cloudflare.com) which have adopted the same APIs used in the browser! +It is also possible to use the `FetchClient` in Node.js v18+, which added [experimental support](https://nodejs.org/en/blog/announcements/v18-release-announce/#fetch-experimental) for `fetch`, although some browser-specific features may not be available. + ## Installation [![http4s-dom Scala version support](https://index.scala-lang.org/http4s/http4s-dom/http4s-dom/latest.svg)](https://index.scala-lang.org/http4s/http4s-dom/http4s-dom) ```scala -// Supports http4s 0.23.x and scala-js-dom 2.x libraryDependencies += "org.http4s" %%% "http4s-dom" % "@VERSION@" // recommended, brings in the latest client module diff --git a/docs/serviceworker.md b/docs/serviceworker.md index 6e8224de..64f4324d 100644 --- a/docs/serviceworker.md +++ b/docs/serviceworker.md @@ -1,6 +1,6 @@ # Service Worker "Server" -The [`ServiceWorker`](https://www.javadoc.io/doc/org.http4s/http4s-dom_sjs1_2.13/latest/org/http4s/dom/ServiceWorker$.html) `FetchEvent` listener integrates directly with [http4s services](https://http4s.org/v0.23/service/). You can use it to run a "proxy server" as a background process in the browser that can intercept and respond to requests made by the `FetchClient`. +The @:api(org.http4s.dom.ServiceWorker$) `FetchEvent` listener integrates directly with [http4s services](https://http4s.org/v0.23/docs/service.html). You can use it to run a "proxy server" as a background process in the browser that can intercept and respond to requests made by the `FetchClient`. ## Example diff --git a/docs/websocket.md b/docs/websocket.md index 658a0c70..f38ce38a 100644 --- a/docs/websocket.md +++ b/docs/websocket.md @@ -1,6 +1,6 @@ # WebSocket Client -The [`WebSocketClient`](https://www.javadoc.io/doc/org.http4s/http4s-dom_sjs1_2.13/latest/org/http4s/dom/WSClient$.html) creates a standard http4s [`WSClientHighLevel`](https://http4s.org/v0.23/api/org/http4s/client/client). +The @:api(org.http4s.dom.WebSocketClient$) implements the http4s @:api(org.http4s.client.websocket.WSClientHighLevel) interface. ## Example @@ -28,15 +28,15 @@ import org.http4s.dom._ import org.http4s.syntax.all._ import org.scalajs.dom._ -val message = document.getElementById("message").asInstanceOf[html.Input] -val button = document.getElementById("button").asInstanceOf[html.Button] -val sent = document.getElementById("sent").asInstanceOf[html.Element] -val received = document.getElementById("received").asInstanceOf[html.Element] +val message = document.getElementById("message").asInstanceOf[HTMLInputElement] +val button = document.getElementById("button").asInstanceOf[HTMLButtonElement] +val sent = document.getElementById("sent").asInstanceOf[HTMLElement] +val received = document.getElementById("received").asInstanceOf[HTMLElement] val request = WSRequest(uri"wss://ws.postman-echo.com/raw") val app = WebSocketClient[IO].connectHighLevel(request).use { conn => - def log(e: html.Element, text: String): IO[Unit] = + def log(e: HTMLElement, text: String): IO[Unit] = IO { val p = document.createElement("p") p.innerHTML = text diff --git a/dom/src/main/scala/org/http4s/dom/FetchClient.scala b/dom/src/main/scala/org/http4s/dom/FetchClient.scala index bd22ae7b..71fcce9f 100644 --- a/dom/src/main/scala/org/http4s/dom/FetchClient.scala +++ b/dom/src/main/scala/org/http4s/dom/FetchClient.scala @@ -17,6 +17,7 @@ package org.http4s package dom +import cats.data.OptionT import cats.effect.Async import cats.effect.Poll import cats.effect.Resource @@ -92,7 +93,7 @@ private[dom] object FetchClient { } } { case (r, exitCase) => - closeReadableStream(r.body, exitCase) + OptionT.fromOption(Option(r.body)).foreachF(closeReadableStream(_, exitCase)) } .evalMap(fromDomResponse[F]) diff --git a/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala b/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala index b6e18ede..41ce3dfb 100644 --- a/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala +++ b/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala @@ -108,9 +108,9 @@ object WebSocketClient { .flatMap(close.complete(_)) *> messages.offer(None) F.delay(ws.readyState).flatMap { - case 0 | 1 => shutdown // CONNECTING | OPEN - case 2 => close.get.void // CLOSING - case 3 => F.unit // CLOSED + case WebSocket.CONNECTING | WebSocket.OPEN => shutdown + case WebSocket.CLOSING => close.get.void + case WebSocket.CLOSED => F.unit case s => F.raiseError(new IllegalStateException(s"WebSocket.readyState: $s")) } } diff --git a/dom/src/main/scala/org/http4s/dom/package.scala b/dom/src/main/scala/org/http4s/dom/package.scala index 6b60e580..51aeeff2 100644 --- a/dom/src/main/scala/org/http4s/dom/package.scala +++ b/dom/src/main/scala/org/http4s/dom/package.scala @@ -54,7 +54,7 @@ package object dom { Response[F]( status = status, headers = fromDomHeaders(response.headers), - entity = Entity(fromReadableStream(response.body), None) + entity = Option(response.body).foldMap(b => Entity(fromReadableStream(b), None)) ) } diff --git a/project/JSEnv.scala b/project/JSEnv.scala index d3b82871..d16337cd 100644 --- a/project/JSEnv.scala +++ b/project/JSEnv.scala @@ -16,6 +16,7 @@ sealed abstract class JSEnv object JSEnv { + case object NodeJS extends JSEnv case object Chrome extends JSEnv case object Firefox extends JSEnv } diff --git a/project/plugins.sbt b/project/plugins.sbt index 45425af0..4feb1d15 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,13 +1,13 @@ -val http4sVersion = "0.23.11" +val http4sVersion = "0.23.12" enablePlugins(BuildInfoPlugin) buildInfoKeys += "http4sVersion" -> http4sVersion libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" libraryDependencies += "org.http4s" %% "http4s-dsl" % http4sVersion -libraryDependencies += "org.http4s" %% "http4s-blaze-server" % http4sVersion +libraryDependencies += "org.http4s" %% "http4s-blaze-server" % "0.23.12" -addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.13.0") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") +addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.13.2") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") diff --git a/tests-nodejs/src/test/scala/org/http4s/dom/NodeJSFetchSuite.scala b/tests-nodejs/src/test/scala/org/http4s/dom/NodeJSFetchSuite.scala new file mode 100644 index 00000000..52007a93 --- /dev/null +++ b/tests-nodejs/src/test/scala/org/http4s/dom/NodeJSFetchSuite.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2021 http4s.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.http4s +package dom + +import cats.effect.IO +import org.http4s.client.testkit.ClientRouteTestBattery + +class NodeJSFetchSuite extends ClientRouteTestBattery("FetchClient") { + def clientResource = FetchClientBuilder[IO].resource +} diff --git a/tests/src/main/scala/org/http4s/dom/TestRoutes.scala b/tests/src/main/scala/org/http4s/dom/TestRoutes.scala index a3590f00..67036087 100644 --- a/tests/src/main/scala/org/http4s/dom/TestRoutes.scala +++ b/tests/src/main/scala/org/http4s/dom/TestRoutes.scala @@ -18,48 +18,20 @@ package org.http4s package dom import cats.effect._ -import cats.syntax.all._ -import fs2._ import org.http4s.Status._ - -import scala.concurrent.duration._ +import org.http4s.client.testkit.testroutes.GetRoutes object TestRoutes { - def apply[F[_]](implicit F: Async[F]): HttpRoutes[F] = HttpRoutes.of { + def routes: HttpRoutes[IO] = HttpRoutes.of { case request => val get = Some(request).filter(_.method == Method.GET).flatMap { r => - GetRoutes.getPaths.get(r.uri.path.segments.last.toString) + GetRoutes.getPaths.get(s"/${r.uri.path.segments.last}") } val post = Some(request).filter(_.method == Method.POST).map { r => - F.delay(Response(entity = r.entity)) + IO(Response(entity = r.entity)) } - get.orElse(post).getOrElse(F.delay(Response[F](NotFound))) + get.orElse(post).getOrElse(IO(Response[IO](NotFound))) } } - -object GetRoutes { - val SimplePath = "simple" - val ChunkedPath = "chunked" - val DelayedPath = "delayed" - val NoContentPath = "no-content" - val NotFoundPath = "not-found" - val EmptyNotFoundPath = "empty-not-found" - val InternalServerErrorPath = "internal-server-error" - - def getPaths[F[_]](implicit F: Temporal[F]): Map[String, F[Response[F]]] = - Map( - SimplePath -> Response[F](Ok).withEntity("simple path").pure[F], - ChunkedPath -> Response[F](Ok) - .withEntity(Stream.emits("chunk".toSeq.map(_.toString)).covary[F]) - .pure[F], - DelayedPath -> - F.sleep(1.second) *> - Response[F](Ok).withEntity("delayed path").pure[F], - NoContentPath -> Response[F](NoContent).pure[F], - NotFoundPath -> Response[F](NotFound).withEntity("not found").pure[F], - EmptyNotFoundPath -> Response[F](NotFound).pure[F], - InternalServerErrorPath -> Response[F](InternalServerError).pure[F] - ) -} diff --git a/tests/src/main/scala/org/http4s/dom/TestServiceWorker.scala b/tests/src/main/scala/org/http4s/dom/TestServiceWorker.scala index 33f4d41b..7fed35e1 100644 --- a/tests/src/main/scala/org/http4s/dom/TestServiceWorker.scala +++ b/tests/src/main/scala/org/http4s/dom/TestServiceWorker.scala @@ -35,7 +35,7 @@ object TestServiceWorker { ) ) - ServiceWorker.addFetchEventListener(IO.pure(TestRoutes[IO])).void.unsafeRunSync() + ServiceWorker.addFetchEventListener(IO.pure(TestRoutes.routes)).void.unsafeRunSync() } } diff --git a/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala b/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala index d9f0c737..55b93736 100644 --- a/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala +++ b/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala @@ -23,7 +23,8 @@ import fs2.Stream import munit.CatsEffectSuite import org.http4s.Method._ import org.http4s.client.dsl.io._ -import org.http4s.multipart.Multipart +import org.http4s.client.testkit.testroutes.GetRoutes +import org.http4s.multipart.Multiparts import org.http4s.multipart.Part import org.http4s.syntax.all._ import org.scalajs.dom.Event @@ -34,11 +35,6 @@ import scala.scalajs.js class FetchServiceWorkerSuite extends CatsEffectSuite { - def scalaVersion = if (BuildInfo.scalaVersion.startsWith("2")) - BuildInfo.scalaVersion.split("\\.").init.mkString(".") - else - BuildInfo.scalaVersion - val client = FetchClientBuilder[IO].create val baseUrl = uri"/" @@ -50,7 +46,7 @@ class FetchServiceWorkerSuite extends CatsEffectSuite { .navigator .serviceWorker .register( - s"/tests/target/scala-${scalaVersion}/tests-fastopt/main.js", + s"/${BuildInfo.workerDir}/main.js", js.Dynamic.literal(scope = "/") ) } @@ -65,7 +61,7 @@ class FetchServiceWorkerSuite extends CatsEffectSuite { } test("Repeat a simple request") { - val path = GetRoutes.SimplePath + val path = GetRoutes.SimplePath.tail def fetchBody = client.toKleisli(_.as[String]).local { (uri: Uri) => Request(uri = uri) } @@ -93,18 +89,23 @@ class FetchServiceWorkerSuite extends CatsEffectSuite { } test("POST a multipart body") { - val multipart = Multipart[IO](Vector(Part.formData("text", "This is text."))) - client - .expect[String](POST(multipart, baseUrl / "echo").withHeaders(multipart.headers)) - .map(_.contains("This is text.")) - .assert + Multiparts.forSync[IO].flatMap { multiparts => + multiparts + .multipart(Vector(Part.formData("text", "This is text."))) + .flatMap { multipart => + client + .expect[String](POST(multipart, baseUrl / "echo").withHeaders(multipart.headers)) + .map(_.contains("This is text.")) + .assert + } + } } - GetRoutes.getPaths[IO].toList.foreach { + GetRoutes.getPaths.toList.foreach { case (path, expected) => test(s"Execute GET $path") { client - .run(GET(baseUrl / path)) + .run(GET(baseUrl / path.tail)) .use(resp => expected.flatMap(checkResponse(resp, _))) .assert }