diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8dd2d0df..59d8c68ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,32 +17,48 @@ jobs: runs-on: [self-hosted, tartelet] steps: - uses: actions/checkout@v2 - - name: Run tests on iOS Simulator + # - name: Run tests on iOS Simulator + # shell: bash + # run: | + # sudo xcode-select --switch /Applications/Xcode_14.3.app + # xcodebuild -scheme BuiltinRegistryGenerator -destination "platform=macOS" + # xcodebuild test -scheme LiveViewNative-Package -sdk iphonesimulator16.4 -destination "OS=16.4,name=iPhone 14 Pro" -enableCodeCoverage YES -resultBundlePath TestResults + # - uses: kishikawakatsumi/xcresulttool@v1 + # with: + # path: TestResults.xcresult + # continue-on-error: true + # if: success() || failure() + # - name: Validate Coverage % + # shell: bash + # run: | + # COVERAGE=$(xcrun xccov view --report --json TestResults.xcresult | jq '.targets[] | select(.name == "LiveViewNative") | .lineCoverage') + # if [ $(echo "$COVERAGE > $MIN_COVERAGE" | bc) -ne 1 ]; then + # printf "Code coverage $COVERAGE does not meet minimum requirement $MIN_COVERAGE" + # exit -1 + # else + # printf "Code coverage $COVERAGE meets minimum requirement $MIN_COVERAGE" + # fi + # - name: Build for macOS + # shell: bash + # run: | + # xcodebuild -scheme LiveViewNative-Package -sdk macosx13.3 -destination "platform=macOS" + # - name: Build for watchOS + # shell: bash + # run: | + # xcodebuild -scheme LiveViewNative-Package -sdk watchsimulator9.4 -destination "OS=9.4,name=Apple Watch Series 8 (45mm)" + - name: Set up Elixir shell: bash run: | - sudo xcode-select --switch /Applications/Xcode_14.3.app - xcodebuild -scheme BuiltinRegistryGenerator -destination "platform=macOS" - xcodebuild test -scheme LiveViewNative-Package -sdk iphonesimulator16.4 -destination "OS=16.4,name=iPhone 14 Pro" -enableCodeCoverage YES -resultBundlePath TestResults + brew install elixir + - name: Run integration tests on iOS Simulator + shell: bash + working-directory: Tests/IntegrationTests + run: | + cd backend && mix deps.get && mix phx.server & + cd frontend && xcodebuild -scheme BuiltinRegistryGenerator -destination "platform=macOS" + xcodebuild test -scheme frontendUITests -sdk iphonesimulator16.4 -destination "OS=16.4,name=iPhone 14 Pro" -resultBundlePath IntegrationTestResults -retry-tests-on-failure - uses: kishikawakatsumi/xcresulttool@v1 with: - path: TestResults.xcresult + path: Tests/IntegrationTests/frontend/IntegrationTestResults.xcresult continue-on-error: true - if: success() || failure() - - name: Validate Coverage % - shell: bash - run: | - COVERAGE=$(xcrun xccov view --report --json TestResults.xcresult | jq '.targets[] | select(.name == "LiveViewNative") | .lineCoverage') - if [ $(echo "$COVERAGE > $MIN_COVERAGE" | bc) -ne 1 ]; then - printf "Code coverage $COVERAGE does not meet minimum requirement $MIN_COVERAGE" - exit -1 - else - printf "Code coverage $COVERAGE meets minimum requirement $MIN_COVERAGE" - fi - - name: Build for macOS - shell: bash - run: | - xcodebuild -scheme LiveViewNative-Package -sdk macosx13.3 -destination "platform=macOS" - - name: Build for watchOS - shell: bash - run: | - xcodebuild -scheme LiveViewNative-Package -sdk watchsimulator9.4 -destination "OS=9.4,name=Apple Watch Series 8 (45mm)" + if: success() || failure() \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..861b97fda --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,76 @@ +{ + "configurations": [ + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "name": "Debug TutorialRepoGenerator", + "program": "${workspaceFolder:liveview-client-swiftui}/.build/debug/TutorialRepoGenerator", + "args": [], + "cwd": "${workspaceFolder:liveview-client-swiftui}", + "preLaunchTask": "swift: Build Debug TutorialRepoGenerator" + }, + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "name": "Release TutorialRepoGenerator", + "program": "${workspaceFolder:liveview-client-swiftui}/.build/release/TutorialRepoGenerator", + "args": [], + "cwd": "${workspaceFolder:liveview-client-swiftui}", + "preLaunchTask": "swift: Build Release TutorialRepoGenerator" + }, + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "name": "Debug DocumentationExtensionGenerator", + "program": "${workspaceFolder:liveview-client-swiftui}/.build/debug/DocumentationExtensionGenerator", + "args": [], + "cwd": "${workspaceFolder:liveview-client-swiftui}", + "preLaunchTask": "swift: Build Debug DocumentationExtensionGenerator" + }, + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "name": "Release DocumentationExtensionGenerator", + "program": "${workspaceFolder:liveview-client-swiftui}/.build/release/DocumentationExtensionGenerator", + "args": [], + "cwd": "${workspaceFolder:liveview-client-swiftui}", + "preLaunchTask": "swift: Build Release DocumentationExtensionGenerator" + }, + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "name": "Debug BuiltinRegistryGenerator", + "program": "${workspaceFolder:liveview-client-swiftui}/.build/debug/BuiltinRegistryGenerator", + "args": [], + "cwd": "${workspaceFolder:liveview-client-swiftui}", + "preLaunchTask": "swift: Build Debug BuiltinRegistryGenerator" + }, + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "name": "Release BuiltinRegistryGenerator", + "program": "${workspaceFolder:liveview-client-swiftui}/.build/release/BuiltinRegistryGenerator", + "args": [], + "cwd": "${workspaceFolder:liveview-client-swiftui}", + "preLaunchTask": "swift: Build Release BuiltinRegistryGenerator" + } + ] +} \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index e1bda0a06..a7bc071bb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,6 +18,15 @@ "version" : "1.2.2" } }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms", + "state" : { + "revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a", + "version" : "0.1.0" + } + }, { "identity" : "swift-cmark", "kind" : "remoteSourceControl", @@ -28,21 +37,21 @@ } }, { - "identity" : "swift-markdown", + "identity" : "swift-collections", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-markdown.git", + "location" : "https://github.com/apple/swift-collections.git", "state" : { - "branch" : "main", - "revision" : "3d4b36cff09f785adf5efb190d458a3d44e6df87" + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" } }, { - "identity" : "swift-syntax", + "identity" : "swift-markdown", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/apple/swift-markdown.git", "state" : { - "revision" : "2e3c42cf38defd998c87ecfe8df138f925d22736", - "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-08-15-a" + "branch" : "main", + "revision" : "3d4b36cff09f785adf5efb190d458a3d44e6df87" } }, { diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index be571d7ff..e1c2d8743 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -364,6 +364,8 @@ public class LiveSessionCoordinator: ObservableObject { // If there is nothing to replace, change the root URL. if !navigationPath.isEmpty { navigationPath[navigationPath.count - 1] = entry + } else { + self.url = redirect.to } } try await coordinator.connect(domValues: self.domValues, redirect: true) @@ -378,6 +380,7 @@ public class LiveSessionCoordinator: ObservableObject { // If there is nothing to replace, change the root URL. if navigationPath.isEmpty { rootCoordinator.url = redirect.to + self.url = redirect.to try await rootCoordinator.connect(domValues: self.domValues, redirect: true) } else { navigationPath.removeLast() @@ -401,6 +404,8 @@ public class LiveSessionCoordinator: ObservableObject { // If there is nothing to replace, change the root URL. if !navigationPath.isEmpty { navigationPath[navigationPath.count - 1] = entry + } else { + self.url = redirect.to } } } diff --git a/Sources/LiveViewNative/LiveView.swift b/Sources/LiveViewNative/LiveView.swift index 7ac36379d..90525faa1 100644 --- a/Sources/LiveViewNative/LiveView.swift +++ b/Sources/LiveViewNative/LiveView.swift @@ -229,7 +229,9 @@ public struct LiveView: View { } private var navigationRoot: some View { - NavStackEntryView(.init(url: storage.session.url, coordinator: rootCoordinator)) + NavStackEntryView( + .init(url: storage.session.url, coordinator: rootCoordinator) + ) } } diff --git a/Tests/IntegrationTests/backend/.formatter.exs b/Tests/IntegrationTests/backend/.formatter.exs new file mode 100644 index 000000000..e945e12b9 --- /dev/null +++ b/Tests/IntegrationTests/backend/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:phoenix], + plugins: [Phoenix.LiveView.HTMLFormatter], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] +] diff --git a/Tests/IntegrationTests/backend/.gitignore b/Tests/IntegrationTests/backend/.gitignore new file mode 100644 index 000000000..8cc2cc4a5 --- /dev/null +++ b/Tests/IntegrationTests/backend/.gitignore @@ -0,0 +1,37 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Temporary files, for example, from tests. +/tmp/ + +# Ignore package tarball (built via "mix hex.build"). +backend-*.tar + +# Ignore assets that are produced by build tools. +/priv/static/assets/ + +# Ignore digested assets cache. +/priv/static/cache_manifest.json + +# In case you use Node.js/npm, you want to ignore these. +npm-debug.log +/assets/node_modules/ + diff --git a/Tests/IntegrationTests/backend/README.md b/Tests/IntegrationTests/backend/README.md new file mode 100644 index 000000000..aabcfb4cf --- /dev/null +++ b/Tests/IntegrationTests/backend/README.md @@ -0,0 +1,18 @@ +# Backend + +To start your Phoenix server: + + * Run `mix setup` to install and setup dependencies + * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` + +Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. + +Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). + +## Learn more + + * Official website: https://www.phoenixframework.org/ + * Guides: https://hexdocs.pm/phoenix/overview.html + * Docs: https://hexdocs.pm/phoenix + * Forum: https://elixirforum.com/c/phoenix-forum + * Source: https://github.com/phoenixframework/phoenix diff --git a/Tests/IntegrationTests/backend/assets/js/app.js b/Tests/IntegrationTests/backend/assets/js/app.js new file mode 100644 index 000000000..df0cdd9f6 --- /dev/null +++ b/Tests/IntegrationTests/backend/assets/js/app.js @@ -0,0 +1,41 @@ +// If you want to use Phoenix channels, run `mix help phx.gen.channel` +// to get started and then uncomment the line below. +// import "./user_socket.js" + +// You can include dependencies in two ways. +// +// The simplest option is to put them in assets/vendor and +// import them using relative paths: +// +// import "../vendor/some-package.js" +// +// Alternatively, you can `npm install some-package --prefix assets` and import +// them using a path starting with the package name: +// +// import "some-package" +// + +// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. +import "phoenix_html" +// Establish Phoenix Socket and LiveView configuration. +import {Socket} from "phoenix" +import {LiveSocket} from "phoenix_live_view" +import topbar from "../vendor/topbar" + +let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) + +// Show progress bar on live navigation and form submits +topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) +window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) +window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) + +// connect if there are any LiveViews on the page +liveSocket.connect() + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket + diff --git a/Tests/IntegrationTests/backend/assets/vendor/topbar.js b/Tests/IntegrationTests/backend/assets/vendor/topbar.js new file mode 100644 index 000000000..41957274d --- /dev/null +++ b/Tests/IntegrationTests/backend/assets/vendor/topbar.js @@ -0,0 +1,165 @@ +/** + * @license MIT + * topbar 2.0.0, 2023-02-04 + * https://buunguyen.github.io/topbar + * Copyright (c) 2021 Buu Nguyen + */ +(function (window, document) { + "use strict"; + + // https://gist.github.com/paulirish/1579671 + (function () { + var lastTime = 0; + var vendors = ["ms", "moz", "webkit", "o"]; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = + window[vendors[x] + "RequestAnimationFrame"]; + window.cancelAnimationFrame = + window[vendors[x] + "CancelAnimationFrame"] || + window[vendors[x] + "CancelRequestAnimationFrame"]; + } + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function (id) { + clearTimeout(id); + }; + })(); + + var canvas, + currentProgress, + showing, + progressTimerId = null, + fadeTimerId = null, + delayTimerId = null, + addEvent = function (elem, type, handler) { + if (elem.addEventListener) elem.addEventListener(type, handler, false); + else if (elem.attachEvent) elem.attachEvent("on" + type, handler); + else elem["on" + type] = handler; + }, + options = { + autoRun: true, + barThickness: 3, + barColors: { + 0: "rgba(26, 188, 156, .9)", + ".25": "rgba(52, 152, 219, .9)", + ".50": "rgba(241, 196, 15, .9)", + ".75": "rgba(230, 126, 34, .9)", + "1.0": "rgba(211, 84, 0, .9)", + }, + shadowBlur: 10, + shadowColor: "rgba(0, 0, 0, .6)", + className: null, + }, + repaint = function () { + canvas.width = window.innerWidth; + canvas.height = options.barThickness * 5; // need space for shadow + + var ctx = canvas.getContext("2d"); + ctx.shadowBlur = options.shadowBlur; + ctx.shadowColor = options.shadowColor; + + var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); + for (var stop in options.barColors) + lineGradient.addColorStop(stop, options.barColors[stop]); + ctx.lineWidth = options.barThickness; + ctx.beginPath(); + ctx.moveTo(0, options.barThickness / 2); + ctx.lineTo( + Math.ceil(currentProgress * canvas.width), + options.barThickness / 2 + ); + ctx.strokeStyle = lineGradient; + ctx.stroke(); + }, + createCanvas = function () { + canvas = document.createElement("canvas"); + var style = canvas.style; + style.position = "fixed"; + style.top = style.left = style.right = style.margin = style.padding = 0; + style.zIndex = 100001; + style.display = "none"; + if (options.className) canvas.classList.add(options.className); + document.body.appendChild(canvas); + addEvent(window, "resize", repaint); + }, + topbar = { + config: function (opts) { + for (var key in opts) + if (options.hasOwnProperty(key)) options[key] = opts[key]; + }, + show: function (delay) { + if (showing) return; + if (delay) { + if (delayTimerId) return; + delayTimerId = setTimeout(() => topbar.show(), delay); + } else { + showing = true; + if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); + if (!canvas) createCanvas(); + canvas.style.opacity = 1; + canvas.style.display = "block"; + topbar.progress(0); + if (options.autoRun) { + (function loop() { + progressTimerId = window.requestAnimationFrame(loop); + topbar.progress( + "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) + ); + })(); + } + } + }, + progress: function (to) { + if (typeof to === "undefined") return currentProgress; + if (typeof to === "string") { + to = + (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 + ? currentProgress + : 0) + parseFloat(to); + } + currentProgress = to > 1 ? 1 : to; + repaint(); + return currentProgress; + }, + hide: function () { + clearTimeout(delayTimerId); + delayTimerId = null; + if (!showing) return; + showing = false; + if (progressTimerId != null) { + window.cancelAnimationFrame(progressTimerId); + progressTimerId = null; + } + (function loop() { + if (topbar.progress("+.1") >= 1) { + canvas.style.opacity -= 0.05; + if (canvas.style.opacity <= 0.05) { + canvas.style.display = "none"; + fadeTimerId = null; + return; + } + } + fadeTimerId = window.requestAnimationFrame(loop); + })(); + }, + }; + + if (typeof module === "object" && typeof module.exports === "object") { + module.exports = topbar; + } else if (typeof define === "function" && define.amd) { + define(function () { + return topbar; + }); + } else { + this.topbar = topbar; + } +}.call(this, window, document)); diff --git a/Tests/IntegrationTests/backend/config/config.exs b/Tests/IntegrationTests/backend/config/config.exs new file mode 100644 index 000000000..f1c4b3f63 --- /dev/null +++ b/Tests/IntegrationTests/backend/config/config.exs @@ -0,0 +1,42 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. + +# General application configuration +import Config + +# Configures the endpoint +config :backend, BackendWeb.Endpoint, + url: [host: "localhost"], + render_errors: [ + formats: [html: BackendWeb.ErrorHTML, json: BackendWeb.ErrorJSON], + layout: false + ], + pubsub_server: Backend.PubSub, + live_view: [signing_salt: "S1xlEgXm"] + +# Configure esbuild (the version is required) +config :esbuild, + version: "0.17.11", + default: [ + args: + ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), + cd: Path.expand("../assets", __DIR__), + env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} + ] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" + +import_config "native.exs" \ No newline at end of file diff --git a/Tests/IntegrationTests/backend/config/dev.exs b/Tests/IntegrationTests/backend/config/dev.exs new file mode 100644 index 000000000..f3be8960a --- /dev/null +++ b/Tests/IntegrationTests/backend/config/dev.exs @@ -0,0 +1,65 @@ +import Config + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we use it +# with esbuild to bundle .js and .css sources. +config :backend, BackendWeb.Endpoint, + # Binding to loopback ipv4 address prevents access from other machines. + # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. + http: [ip: {127, 0, 0, 1}, port: 4000], + check_origin: false, + code_reloader: true, + debug_errors: true, + secret_key_base: "dF16OOk4GPJ+ZkIAExNpp+vOGV32YWzjpLNDKL+WSZX09RE0Fa/krUmnut7rOkSp", + watchers: [ + esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} + ] + +# ## SSL Support +# +# In order to use HTTPS in development, a self-signed +# certificate can be generated by running the following +# Mix task: +# +# mix phx.gen.cert +# +# Run `mix help phx.gen.cert` for more information. +# +# The `http:` config above can be replaced with: +# +# https: [ +# port: 4001, +# cipher_suite: :strong, +# keyfile: "priv/cert/selfsigned_key.pem", +# certfile: "priv/cert/selfsigned.pem" +# ], +# +# If desired, both `http:` and `https:` keys can be +# configured to run both http and https servers on +# different ports. + +# Watch static and templates for browser reloading. +config :backend, BackendWeb.Endpoint, + live_reload: [ + patterns: [ + ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"priv/gettext/.*(po)$", + ~r"lib/backend_web/(controllers|live|components)/.*(ex|heex)$" + ] + ] + +# Enable dev routes for dashboard and mailbox +config :backend, dev_routes: true + +# Do not include metadata nor timestamps in development logs +config :logger, :console, format: "[$level] $message\n" + +# Set a higher stacktrace during development. Avoid configuring such +# in production as building large stacktraces may be expensive. +config :phoenix, :stacktrace_depth, 20 + +# Initialize plugs at runtime for faster development compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/Tests/IntegrationTests/backend/config/native.exs b/Tests/IntegrationTests/backend/config/native.exs new file mode 100644 index 000000000..188fddc14 --- /dev/null +++ b/Tests/IntegrationTests/backend/config/native.exs @@ -0,0 +1,5 @@ +# This file is responsible for configuring LiveView Native. +# It is auto-generated when running `mix lvn.install`. +import Config + +config :live_view_native, plugins: [LiveViewNativeSwiftUi] diff --git a/Tests/IntegrationTests/backend/config/prod.exs b/Tests/IntegrationTests/backend/config/prod.exs new file mode 100644 index 000000000..77315a5db --- /dev/null +++ b/Tests/IntegrationTests/backend/config/prod.exs @@ -0,0 +1,18 @@ +import Config + +# For production, don't forget to configure the url host +# to something meaningful, Phoenix uses this information +# when generating URLs. + +# Note we also include the path to a cache manifest +# containing the digested version of static files. This +# manifest is generated by the `mix assets.deploy` task, +# which you should run after static files are built and +# before starting your production server. +config :backend, BackendWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" + +# Do not print debug messages in production +config :logger, level: :info + +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/Tests/IntegrationTests/backend/config/runtime.exs b/Tests/IntegrationTests/backend/config/runtime.exs new file mode 100644 index 000000000..4e7cf80f2 --- /dev/null +++ b/Tests/IntegrationTests/backend/config/runtime.exs @@ -0,0 +1,82 @@ +import Config + +# config/runtime.exs is executed for all environments, including +# during releases. It is executed after compilation and before the +# system starts, so it is typically used to load production configuration +# and secrets from environment variables or elsewhere. Do not define +# any compile-time configuration in here, as it won't be applied. +# The block below contains prod specific runtime configuration. + +# ## Using releases +# +# If you use `mix release`, you need to explicitly enable the server +# by passing the PHX_SERVER=true when you start it: +# +# PHX_SERVER=true bin/backend start +# +# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` +# script that automatically sets the env var above. +if System.get_env("PHX_SERVER") do + config :backend, BackendWeb.Endpoint, server: true +end + +if config_env() == :prod do + # The secret key base is used to sign/encrypt cookies and other secrets. + # A default value is used in config/dev.exs and config/test.exs but you + # want to use a different value for prod and you most likely don't want + # to check this value into version control, so we use an environment + # variable instead. + secret_key_base = + System.get_env("SECRET_KEY_BASE") || + raise """ + environment variable SECRET_KEY_BASE is missing. + You can generate one by calling: mix phx.gen.secret + """ + + host = System.get_env("PHX_HOST") || "example.com" + port = String.to_integer(System.get_env("PORT") || "4000") + + config :backend, BackendWeb.Endpoint, + url: [host: host, port: 443, scheme: "https"], + http: [ + # Enable IPv6 and bind on all interfaces. + # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. + # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html + # for details about using IPv6 vs IPv4 and loopback vs public addresses. + ip: {0, 0, 0, 0, 0, 0, 0, 0}, + port: port + ], + secret_key_base: secret_key_base + + # ## SSL Support + # + # To get SSL working, you will need to add the `https` key + # to your endpoint configuration: + # + # config :backend, BackendWeb.Endpoint, + # https: [ + # ..., + # port: 443, + # cipher_suite: :strong, + # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), + # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") + # ] + # + # The `cipher_suite` is set to `:strong` to support only the + # latest and more secure SSL ciphers. This means old browsers + # and clients may not be supported. You can set it to + # `:compatible` for wider support. + # + # `:keyfile` and `:certfile` expect an absolute path to the key + # and cert in disk or a relative path inside priv, for example + # "priv/ssl/server.key". For all supported SSL configuration + # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 + # + # We also recommend setting `force_ssl` in your endpoint, ensuring + # no data is ever sent via http, always redirecting to https: + # + # config :backend, BackendWeb.Endpoint, + # force_ssl: [hsts: true] + # + # Check `Plug.SSL` for all available options in `force_ssl`. +end diff --git a/Tests/IntegrationTests/backend/config/test.exs b/Tests/IntegrationTests/backend/config/test.exs new file mode 100644 index 000000000..796eb7074 --- /dev/null +++ b/Tests/IntegrationTests/backend/config/test.exs @@ -0,0 +1,14 @@ +import Config + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :backend, BackendWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "l0QMqn3n7hoangwcwa8ypCZyJexEfEvvp4xn0FJQgriJs5+7wKp6+WiJKdpXNt2d", + server: false + +# Print only warnings and errors during test +config :logger, level: :warning + +# Initialize plugs at runtime for faster test compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/Tests/IntegrationTests/backend/lib/backend.ex b/Tests/IntegrationTests/backend/lib/backend.ex new file mode 100644 index 000000000..d059a26e6 --- /dev/null +++ b/Tests/IntegrationTests/backend/lib/backend.ex @@ -0,0 +1,9 @@ +defmodule Backend do + @moduledoc """ + Backend keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ +end diff --git a/Tests/IntegrationTests/backend/lib/backend/application.ex b/Tests/IntegrationTests/backend/lib/backend/application.ex new file mode 100644 index 000000000..27ab399a9 --- /dev/null +++ b/Tests/IntegrationTests/backend/lib/backend/application.ex @@ -0,0 +1,34 @@ +defmodule Backend.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + # Start the Telemetry supervisor + BackendWeb.Telemetry, + # Start the PubSub system + {Phoenix.PubSub, name: Backend.PubSub}, + # Start the Endpoint (http/https) + BackendWeb.Endpoint + # Start a worker by calling: Backend.Worker.start_link(arg) + # {Backend.Worker, arg} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Backend.Supervisor] + Supervisor.start_link(children, opts) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + @impl true + def config_change(changed, _new, removed) do + BackendWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/Tests/IntegrationTests/backend/lib/backend_web.ex b/Tests/IntegrationTests/backend/lib/backend_web.ex new file mode 100644 index 000000000..035c39968 --- /dev/null +++ b/Tests/IntegrationTests/backend/lib/backend_web.ex @@ -0,0 +1,113 @@ +defmodule BackendWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, components, channels, and so on. + + This can be used in your application as: + + use BackendWeb, :controller + use BackendWeb, :html + + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define additional modules and import + those modules here. + """ + + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + end + end + + def controller do + quote do + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: BackendWeb.Layouts] + + import Plug.Conn + import BackendWeb.Gettext + + unquote(verified_routes()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {BackendWeb.Layouts, :app} + + unquote(html_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(html_helpers()) + end + end + + def html do + quote do + use Phoenix.Component + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(html_helpers()) + end + end + + defp html_helpers do + quote do + # HTML escaping functionality + import Phoenix.HTML + # Core UI components and translation + import BackendWeb.CoreComponents + import BackendWeb.Gettext + + # Shortcut for generating JS commands + alias Phoenix.LiveView.JS + + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: BackendWeb.Endpoint, + router: BackendWeb.Router, + statics: BackendWeb.static_paths() + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/Tests/IntegrationTests/backend/lib/backend_web/components/core_components.ex b/Tests/IntegrationTests/backend/lib/backend_web/components/core_components.ex new file mode 100644 index 000000000..8376d3432 --- /dev/null +++ b/Tests/IntegrationTests/backend/lib/backend_web/components/core_components.ex @@ -0,0 +1,640 @@ +defmodule BackendWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + + At the first glance, this module may seem daunting, but its goal is + to provide some core building blocks in your application, such modals, + tables, and forms. The components are mostly markup and well documented + with doc strings and declarative assigns. You may customize and style + them in any way you want, based on your application growth and needs. + + The default components use Tailwind CSS, a utility-first CSS framework. + See the [Tailwind CSS documentation](https://tailwindcss.com) to learn + how to customize them or feel free to swap in another framework altogether. + + Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. + """ + use Phoenix.Component + + alias Phoenix.LiveView.JS + import BackendWeb.Gettext + + @doc """ + Renders a modal. + + ## Examples + + <.modal id="confirm-modal"> + This is a modal. + + + JS commands may be passed to the `:on_cancel` to configure + the closing/cancel event, for example: + + <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> + This is another modal. + + + """ + attr :id, :string, required: true + attr :show, :boolean, default: false + attr :on_cancel, JS, default: %JS{} + slot :inner_block, required: true + + def modal(assigns) do + ~H""" +