diff --git a/.formatter.exs b/.formatter.exs
index 82396f2..c5096d7 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -16,7 +16,7 @@ locals_without_parens = ~w[
details summary menuitem menu
meta link base
area br col embed hr img input keygen param source track wbr
- text partial
+ txt partial
animate animateMotion animateTransform circle clipPath
color-profile defs desc discard ellipse feBlend
@@ -24,7 +24,7 @@ locals_without_parens = ~w[
feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset
fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter foreignObject g hatch hatchpath image line linearGradient
marker mask mesh meshgradient meshpatch meshrow metadata mpath path pattern polygon
- polyline radialGradient rect set solidcolor stop svg switch symbol text_
+ polyline radialGradient rect set solidcolor stop svg switch symbol text
textPath tspan unknown use view
form_for inputs_for
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1238719..d44670d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,9 +24,49 @@ jobs:
id: cache
with:
path: deps
- key: ${{ runner.os }}-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
- restore-keys: |
- ${{ runner.os }}-mix-
+ key: ${{ runner.os }}-test-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+ restore-keys:
+ - name: Install Dependencies
+ if: steps.cache.outputs.cache-hit != 'true'
+ run: mix deps.get
+
+ - name: Run Tests
+ run: mix test
+
+ integration_tests:
+ runs-on: ubuntu-latest
+ name: Integration Test (${{matrix.elixir}}/${{matrix.otp}})
+ defaults:
+ run:
+ working-directory: "./integration_test/temple_demo"
+
+ strategy:
+ matrix:
+ otp: [21.x]
+ elixir: [1.7.x, 1.8.x, 1.9.x, 1.10.x]
+
+ services:
+ db:
+ image: postgres:12
+ env:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: temple_demo_test
+ ports: ['5432:5432']
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
+
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-elixir@v1.2.0
+ with:
+ otp-version: ${{matrix.otp}}
+ elixir-version: ${{matrix.elixir}}
+ - uses: actions/cache@v1
+ id: cache
+ with:
+ path: deps
+ key: ${{ runner.os }}-integration-test-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/integration_test/temple_demo/mix.lock')) }}
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
@@ -34,6 +74,14 @@ jobs:
- name: Run Tests
run: mix test
+ env:
+ MIX_ENV: test
+
+ - uses: actions/upload-artifact@v2
+ if: failure()
+ with:
+ name: screenshots
+ path: screenshots/
formatter:
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 0652411..55093f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
# The directory Mix downloads your dependencies sources to.
/deps/
+/integration_test/temple_demo/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
diff --git a/.tool-versions b/.tool-versions
index 4eafd51..8ca6814 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,2 @@
elixir 1.10.2
-erlang 22.3.1
+erlang 23.0.1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f86902f..3441355 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
## Master
+## 0.6.0-alpha.0
+
+### Breaking!
+
+This version is the start of a complete rewrite of Temple.
+
+- Compiles to EEx at build time.
+- Compatible with `Phoenix.LiveView`
+- All modules other than `Temple` are removed
+- `mix temple.convert` Mix task removed
+
## 0.5.0
- Introduce `@assigns` assign
diff --git a/README.md b/README.md
index 9884a7f..1092594 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,6 @@
# ![](temple.png)
-> Temple is now undergoing a rewrite. The goal is to compile to EEx at compile time, so that it can then be fed straight into the Phoenix HTML and LiveView engines. This way, Temple becomes compatible with LiveView as well as gaining all the same optimizations as normal Phoenix templates.
->
-> There is no guarantee that the rewrite will maintain the current feature set of Temple, mainly the Component API.
->
-> To follow along, please checkout out the [rewrite](https://github.com/mhanberg/temple/tree/rewrite) branch. The code will likely be in a "spike" state until further notice, so don't let the WIP commits scare you off 😄
-
----
+> You are looking at the README for the master branch. The README for the latest stable release is located [here](https://github.com/mhanberg/temple/tree/v0.5.0).
[![Actions Status](https://github.com/mhanberg/temple/workflows/CI/badge.svg)](https://github.com/mhanberg/temple/actions)
[![Hex.pm](https://img.shields.io/hexpm/v/temple.svg)](https://hex.pm/packages/temple)
@@ -14,7 +8,7 @@
Temple is a DSL for writing HTML using Elixir.
-You're probably here because you want to use Temple to write Phoenix templates, which is why Temple includes a [Phoenix template engine](#phoenix-templates) and Temple-compatible [Phoenix form helpers](#phoenixhtml).
+You're probably here because you want to use Temple to write Phoenix templates, which is why Temple includes a [Phoenix template engine](#phoenix-templates).
## Installation
@@ -22,13 +16,13 @@ Add `temple` to your list of dependencies in `mix.exs`:
```elixir
def deps do
- [{:temple, "~> 0.5.0"}]
+ [{:temple, "~> 0.6.0-alpha.0"}]
end
```
## Usage
-Using Temple is a as simple as using the DSL inside of an `temple/1` block. This returns a safe result of the form `{:safe, html_string}`.
+Using Temple is a as simple as using the DSL inside of an `temple/1` block. This returns an EEx string at compile time.
See the [documentation](https://hexdocs.pm/temple/Temple.Html.html) for more details.
@@ -36,7 +30,7 @@ See the [documentation](https://hexdocs.pm/temple/Temple.Html.html) for more det
use Temple
temple do
- h2 "todos"
+ h2 do: "todos"
ul class: "list" do
for item <- @items do
@@ -45,12 +39,12 @@ temple do
div class: "bullet hidden"
end
- div item
+ div do: item
end
end
end
- script """
+ script do: """
function toggleCheck({currentTarget}) {
currentTarget.children[0].children[0].classList.toggle("hidden");
}
@@ -62,81 +56,37 @@ temple do
end
```
-### Components
-
-Temple provides an API for creating custom components that act as custom HTML elements.
-
-These components can be given `assigns` that are available inside the component definition as module attributes. The contents of a components `do` block are available as a special `@children` assign.
-
-See the [documentation](https://hexdocs.pm/temple/Temple.html#defcomponent/2) for more details.
-
-```elixir
-defcomponent :flex do
- div id: @id, class: "flex" do
- @children
- end
-end
-
-temple do
- flex id: "my-flex" do
- div "Item 1"
- div "Item 2"
- div "Item 3"
- end
-end
-```
-
-### Phoenix.HTML
-
-Temple provides macros for working with the helpers provided by the [Phoenix.HTML](https://www.github.com/phoenixframework/phoenix_html) package.
-
-Most of the macros are purely wrappers, while the semantics of some are changed to work with Temple.
-
-See the [documentation](https://hexdocs.pm/temple/Temple.Form.html#content) for more details.
-
-```elixir
-temple do
- form_for @conn, Routes.some_path(@conn, :create) do
- text_input form, :name
- end
-end
-```
-
### Phoenix templates
Add the templating engine to your Phoenix configuration.
-See the [documentation](https://hexdocs.pm/temple/Temple.Engine.html#content) for more details.
+See the [Temple.Engine](https://hexdocs.pm/temple/Temple.Engine.html#content) and [Temple.LiveEngine](https://hexdocs.pm/temple/Temple.LiveEngine.html#content) for more details.
```elixir
# config.exs
-config :phoenix, :template_engines, exs: Temple.Engine
+config :phoenix, :template_engines,
+ exs: Temple.Engine
+ exs: Temple.LiveEngine
# config/dev.exs
config :your_app, YourAppWeb.Endpoint,
live_reload: [
patterns: [
- ~r"lib/your_app_web/templates/.*(exs)$"
+ ~r"lib/myapp_web/(live|views)/.*(ex|exs)$",
+ ~r"lib/myapp_web/templates/.*(eex|exs)$"
]
]
-
-# your_app_web.ex
-def view do
- quote do
- # ...
- use Temple # Replaces the call to import Phoenix.HTML
- end
-end
```
```elixir
# app.html.exs
+
html lang: "en" do
head do
meta charset: "utf-8"
meta http_equiv: "X-UA-Compatible", content: "IE=edge"
meta name: "viewport", content: "width=device-width, initial-scale=1.0"
- title "YourApp · Phoenix Framework"
+ title do: "YourApp · Phoenix Framework"
link rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css")
end
@@ -158,10 +108,15 @@ html lang: "en" do
end
main role: "main", class: "container" do
- p get_flash(@conn, :info), class: "alert alert-info", role: "alert"
- p get_flash(@conn, :error), class: "alert alert-danger", role: "alert"
+ p class: "alert alert-info", role: "alert" do
+ get_flash(@conn, :info)
+ end
- partial render(@view_module, @view_template, assigns)
+ p class: "alert alert-danger", role: "alert" do
+ get_flash(@conn, :error)
+ end
+
+ render @view_module, @view_template, assigns
end
script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")
@@ -171,11 +126,13 @@ end
### Tasks
-#### temple.convert
+#### temple.gen.layout
-This task can be used to convert plain HTML and SVG into Temple syntax. Input is taken from stdin or from a file and the output is sent to stdout.
+Generates the app layout.
-`cat index.html | mix temple.convert > index.html.exs`
+#### temple.gen.html
+
+Generates the templates for a resource.
### Formatter
diff --git a/bin/integration-test b/bin/integration-test
new file mode 100755
index 0000000..5e9a4cc
--- /dev/null
+++ b/bin/integration-test
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -e
+
+(cd integration_test/temple_demo && mix test)
diff --git a/bin/test b/bin/test
new file mode 100755
index 0000000..b9c2615
--- /dev/null
+++ b/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -e
+
+mix test
diff --git a/bin/test-all b/bin/test-all
new file mode 100755
index 0000000..b027cb6
--- /dev/null
+++ b/bin/test-all
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -e
+
+mix test
+
+(cd integration_test/temple_demo && mix test)
diff --git a/integration_test/temple_demo/.formatter.exs b/integration_test/temple_demo/.formatter.exs
new file mode 100644
index 0000000..09a57c5
--- /dev/null
+++ b/integration_test/temple_demo/.formatter.exs
@@ -0,0 +1,5 @@
+[
+ import_deps: [:ecto, :phoenix, :temple],
+ inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
+ subdirectories: ["priv/*/migrations"]
+]
diff --git a/integration_test/temple_demo/.gitignore b/integration_test/temple_demo/.gitignore
new file mode 100644
index 0000000..50c2c65
--- /dev/null
+++ b/integration_test/temple_demo/.gitignore
@@ -0,0 +1,26 @@
+# 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
+
+# Ignore package tarball (built via "mix hex.build").
+temple_demo-*.tar
+
+/screenshots/
+
diff --git a/integration_test/temple_demo/README.md b/integration_test/temple_demo/README.md
new file mode 100644
index 0000000..0f76960
--- /dev/null
+++ b/integration_test/temple_demo/README.md
@@ -0,0 +1,18 @@
+# TempleDemo
+
+To start your Phoenix server:
+
+ * Setup the project with `mix setup`
+ * Start Phoenix endpoint with `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/integration_test/temple_demo/config/config.exs b/integration_test/temple_demo/config/config.exs
new file mode 100644
index 0000000..c4db027
--- /dev/null
+++ b/integration_test/temple_demo/config/config.exs
@@ -0,0 +1,42 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Mix.Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+use Mix.Config
+
+config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
+
+config :temple_demo,
+ ecto_repos: [TempleDemo.Repo]
+
+config :phoenix, :template_engines,
+ exs: Temple.Engine
+
+# Configures the endpoint
+config :temple_demo, TempleDemoWeb.Endpoint,
+ url: [host: "localhost"],
+ secret_key_base: "ww1nKdikInNFHHUfSdCE1wiTcOmQq/KLvOxG7CY1TlKLDTmLW5yheCCYpfoxmZAW",
+ render_errors: [view: TempleDemoWeb.ErrorView, accepts: ~w(html json), layout: false],
+ pubsub_server: TempleDemo.PubSub,
+ live_view: [signing_salt: "KCU/YIG0"]
+
+# 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
+
+config :temple, :aliases,
+ label: :_label,
+ link: :_link,
+ textarea: :_textarea
+
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{Mix.env()}.exs"
diff --git a/integration_test/temple_demo/config/dev.exs b/integration_test/temple_demo/config/dev.exs
new file mode 100644
index 0000000..96993d8
--- /dev/null
+++ b/integration_test/temple_demo/config/dev.exs
@@ -0,0 +1,68 @@
+use Mix.Config
+
+# Configure your database
+config :temple_demo, TempleDemo.Repo,
+ username: "postgres",
+ password: "postgres",
+ database: "temple_demo_dev",
+ hostname: "localhost",
+ show_sensitive_data_on_connection_error: true,
+ pool_size: 10
+
+# 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 webpack to recompile .js and .css sources.
+config :temple_demo, TempleDemoWeb.Endpoint,
+ http: [port: 4000],
+ debug_errors: true,
+ code_reloader: true,
+ check_origin: false,
+ watchers: []
+
+# ## 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
+#
+# Note that this task requires Erlang/OTP 20 or later.
+# 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 :temple_demo, TempleDemoWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/gettext/.*(po)$",
+ ~r"lib/temple_demo_web/(live|views)/.*(ex)$",
+ ~r"lib/temple_demo_web/templates/.*(eex|exs)$"
+ ]
+ ]
+
+# 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/integration_test/temple_demo/config/prod.exs b/integration_test/temple_demo/config/prod.exs
new file mode 100644
index 0000000..de2626c
--- /dev/null
+++ b/integration_test/temple_demo/config/prod.exs
@@ -0,0 +1,55 @@
+use Mix.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 phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :temple_demo, TempleDemoWeb.Endpoint,
+ url: [host: "example.com", port: 80],
+ cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# ## SSL Support
+#
+# To get SSL working, you will need to add the `https` key
+# to the previous section and set your `:url` port to 443:
+#
+# config :temple_demo, TempleDemoWeb.Endpoint,
+# ...
+# url: [host: "example.com", port: 443],
+# https: [
+# port: 443,
+# cipher_suite: :strong,
+# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
+# transport_options: [socket_opts: [:inet6]]
+# ]
+#
+# 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 :temple_demo, TempleDemoWeb.Endpoint,
+# force_ssl: [hsts: true]
+#
+# Check `Plug.SSL` for all available options in `force_ssl`.
+
+# Finally import the config/prod.secret.exs which loads secrets
+# and configuration from environment variables.
+import_config "prod.secret.exs"
diff --git a/integration_test/temple_demo/config/prod.secret.exs b/integration_test/temple_demo/config/prod.secret.exs
new file mode 100644
index 0000000..06dc174
--- /dev/null
+++ b/integration_test/temple_demo/config/prod.secret.exs
@@ -0,0 +1,41 @@
+# In this file, we load production configuration and secrets
+# from environment variables. You can also hardcode secrets,
+# although such is generally not recommended and you have to
+# remember to add this file to your .gitignore.
+use Mix.Config
+
+database_url =
+ System.get_env("DATABASE_URL") ||
+ raise """
+ environment variable DATABASE_URL is missing.
+ For example: ecto://USER:PASS@HOST/DATABASE
+ """
+
+config :temple_demo, TempleDemo.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
+
+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
+ """
+
+config :temple_demo, TempleDemoWeb.Endpoint,
+ http: [
+ port: String.to_integer(System.get_env("PORT") || "4000"),
+ transport_options: [socket_opts: [:inet6]]
+ ],
+ secret_key_base: secret_key_base
+
+# ## Using releases (Elixir v1.9+)
+#
+# If you are doing OTP releases, you need to instruct Phoenix
+# to start each relevant endpoint:
+#
+# config :temple_demo, TempleDemoWeb.Endpoint, server: true
+#
+# Then you can assemble a release by calling `mix release`.
+# See `mix help release` for more information.
diff --git a/integration_test/temple_demo/config/test.exs b/integration_test/temple_demo/config/test.exs
new file mode 100644
index 0000000..22dc0e5
--- /dev/null
+++ b/integration_test/temple_demo/config/test.exs
@@ -0,0 +1,30 @@
+use Mix.Config
+
+# Configure your database
+#
+# The MIX_TEST_PARTITION environment variable can be used
+# to provide built-in test partitioning in CI environment.
+# Run `mix help test` for more information.
+config :temple_demo, TempleDemo.Repo,
+ username: "postgres",
+ password: "postgres",
+ database: "temple_demo_test#{System.get_env("MIX_TEST_PARTITION")}",
+ hostname: "localhost",
+ pool: Ecto.Adapters.SQL.Sandbox
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :temple_demo, TempleDemoWeb.Endpoint,
+ http: [port: 4002],
+ server: true
+
+config :temple_demo, :sql_sandbox, true
+
+config :wallaby,
+ base_url: "http://localhost:4002",
+ otp_app: :temple_demo,
+ screenshot_on_failure: true
+
+
+# Print only warnings and errors during test
+config :logger, level: :warn
diff --git a/integration_test/temple_demo/lib/temple_demo.ex b/integration_test/temple_demo/lib/temple_demo.ex
new file mode 100644
index 0000000..6552333
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo.ex
@@ -0,0 +1,9 @@
+defmodule TempleDemo do
+ @moduledoc """
+ TempleDemo 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/integration_test/temple_demo/lib/temple_demo/application.ex b/integration_test/temple_demo/lib/temple_demo/application.ex
new file mode 100644
index 0000000..75194f7
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo/application.ex
@@ -0,0 +1,34 @@
+defmodule TempleDemo.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ def start(_type, _args) do
+ children = [
+ # Start the Ecto repository
+ TempleDemo.Repo,
+ # Start the Telemetry supervisor
+ TempleDemoWeb.Telemetry,
+ # Start the PubSub system
+ {Phoenix.PubSub, name: TempleDemo.PubSub},
+ # Start the Endpoint (http/https)
+ TempleDemoWeb.Endpoint
+ # Start a worker by calling: TempleDemo.Worker.start_link(arg)
+ # {TempleDemo.Worker, arg}
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: TempleDemo.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ def config_change(changed, _new, removed) do
+ TempleDemoWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo/blog.ex b/integration_test/temple_demo/lib/temple_demo/blog.ex
new file mode 100644
index 0000000..dfc94b6
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo/blog.ex
@@ -0,0 +1,104 @@
+defmodule TempleDemo.Blog do
+ @moduledoc """
+ The Blog context.
+ """
+
+ import Ecto.Query, warn: false
+ alias TempleDemo.Repo
+
+ alias TempleDemo.Blog.Post
+
+ @doc """
+ Returns the list of posts.
+
+ ## Examples
+
+ iex> list_posts()
+ [%Post{}, ...]
+
+ """
+ def list_posts do
+ Repo.all(Post)
+ end
+
+ @doc """
+ Gets a single post.
+
+ Raises `Ecto.NoResultsError` if the Post does not exist.
+
+ ## Examples
+
+ iex> get_post!(123)
+ %Post{}
+
+ iex> get_post!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_post!(id), do: Repo.get!(Post, id)
+
+ @doc """
+ Creates a post.
+
+ ## Examples
+
+ iex> create_post(%{field: value})
+ {:ok, %Post{}}
+
+ iex> create_post(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_post(attrs \\ %{}) do
+ %Post{}
+ |> Post.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a post.
+
+ ## Examples
+
+ iex> update_post(post, %{field: new_value})
+ {:ok, %Post{}}
+
+ iex> update_post(post, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_post(%Post{} = post, attrs) do
+ post
+ |> Post.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a post.
+
+ ## Examples
+
+ iex> delete_post(post)
+ {:ok, %Post{}}
+
+ iex> delete_post(post)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_post(%Post{} = post) do
+ Repo.delete(post)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking post changes.
+
+ ## Examples
+
+ iex> change_post(post)
+ %Ecto.Changeset{data: %Post{}}
+
+ """
+ def change_post(%Post{} = post, attrs \\ %{}) do
+ Post.changeset(post, attrs)
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo/blog/post.ex b/integration_test/temple_demo/lib/temple_demo/blog/post.ex
new file mode 100644
index 0000000..6e85e82
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo/blog/post.ex
@@ -0,0 +1,20 @@
+defmodule TempleDemo.Blog.Post do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "posts" do
+ field :author, :string
+ field :body, :string
+ field :published_at, :naive_datetime
+ field :title, :string
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(post, attrs) do
+ post
+ |> cast(attrs, [:title, :body, :published_at, :author])
+ |> validate_required([:title, :body, :published_at, :author])
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo/repo.ex b/integration_test/temple_demo/lib/temple_demo/repo.ex
new file mode 100644
index 0000000..fe372f1
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo/repo.ex
@@ -0,0 +1,5 @@
+defmodule TempleDemo.Repo do
+ use Ecto.Repo,
+ otp_app: :temple_demo,
+ adapter: Ecto.Adapters.Postgres
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web.ex b/integration_test/temple_demo/lib/temple_demo_web.ex
new file mode 100644
index 0000000..2edbee6
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web.ex
@@ -0,0 +1,80 @@
+defmodule TempleDemoWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, views, channels and so on.
+
+ This can be used in your application as:
+
+ use TempleDemoWeb, :controller
+ use TempleDemoWeb, :view
+
+ The definitions below will be executed for every view,
+ controller, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define any helper function in modules
+ and import those modules here.
+ """
+
+ def controller do
+ quote do
+ use Phoenix.Controller, namespace: TempleDemoWeb
+
+ import Plug.Conn
+ import TempleDemoWeb.Gettext
+ alias TempleDemoWeb.Router.Helpers, as: Routes
+ end
+ end
+
+ def view do
+ quote do
+ use Phoenix.View,
+ root: "lib/temple_demo_web/templates",
+ namespace: TempleDemoWeb
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
+
+ # Include shared imports and aliases for views
+ unquote(view_helpers())
+ end
+ end
+
+ def router do
+ quote do
+ use Phoenix.Router
+
+ import Plug.Conn
+ import Phoenix.Controller
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ import TempleDemoWeb.Gettext
+ end
+ end
+
+ defp view_helpers do
+ quote do
+ # Use all HTML functionality (forms, tags, etc)
+ use Phoenix.HTML
+
+ # Import basic rendering functionality (render, render_layout, etc)
+ import Phoenix.View
+
+ import TempleDemoWeb.ErrorHelpers
+ import TempleDemoWeb.Gettext
+ alias TempleDemoWeb.Router.Helpers, as: Routes
+ 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/integration_test/temple_demo/lib/temple_demo_web/channels/user_socket.ex b/integration_test/temple_demo/lib/temple_demo_web/channels/user_socket.ex
new file mode 100644
index 0000000..8e9d8ff
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/channels/user_socket.ex
@@ -0,0 +1,35 @@
+defmodule TempleDemoWeb.UserSocket do
+ use Phoenix.Socket
+
+ ## Channels
+ # channel "room:*", TempleDemoWeb.RoomChannel
+
+ # Socket params are passed from the client and can
+ # be used to verify and authenticate a user. After
+ # verification, you can put default assigns into
+ # the socket that will be set for all channels, ie
+ #
+ # {:ok, assign(socket, :user_id, verified_user_id)}
+ #
+ # To deny connection, return `:error`.
+ #
+ # See `Phoenix.Token` documentation for examples in
+ # performing token verification on connect.
+ @impl true
+ def connect(_params, socket, _connect_info) do
+ {:ok, socket}
+ end
+
+ # Socket id's are topics that allow you to identify all sockets for a given user:
+ #
+ # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
+ #
+ # Would allow you to broadcast a "disconnect" event and terminate
+ # all active sockets and channels for a given user:
+ #
+ # TempleDemoWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
+ #
+ # Returning `nil` makes this socket anonymous.
+ @impl true
+ def id(_socket), do: nil
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/controllers/page_controller.ex b/integration_test/temple_demo/lib/temple_demo_web/controllers/page_controller.ex
new file mode 100644
index 0000000..ef6fafd
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/controllers/page_controller.ex
@@ -0,0 +1,7 @@
+defmodule TempleDemoWeb.PageController do
+ use TempleDemoWeb, :controller
+
+ def index(conn, _params) do
+ render(conn, "index.html")
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/controllers/post_controller.ex b/integration_test/temple_demo/lib/temple_demo_web/controllers/post_controller.ex
new file mode 100644
index 0000000..c93a80d
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/controllers/post_controller.ex
@@ -0,0 +1,63 @@
+defmodule TempleDemoWeb.PostController do
+ use TempleDemoWeb, :controller
+
+ alias TempleDemo.Blog
+ alias TempleDemo.Blog.Post
+
+ def index(conn, _params) do
+ posts = Blog.list_posts()
+ render(conn, "index.html", posts: posts)
+ end
+
+ def new(conn, _params) do
+ changeset = Blog.change_post(%Post{})
+
+ render(conn, "new.html", changeset: changeset)
+ end
+
+ def create(conn, %{"post" => post_params}) do
+ case Blog.create_post(post_params) do
+ {:ok, post} ->
+ conn
+ |> put_flash(:info, "Post created successfully.")
+ |> redirect(to: Routes.post_path(conn, :show, post))
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, "new.html", changeset: changeset)
+ end
+ end
+
+ def show(conn, %{"id" => id}) do
+ post = Blog.get_post!(id)
+ render(conn, "show.html", post: post)
+ end
+
+ def edit(conn, %{"id" => id}) do
+ post = Blog.get_post!(id)
+ changeset = Blog.change_post(post)
+ render(conn, "edit.html", post: post, changeset: changeset)
+ end
+
+ def update(conn, %{"id" => id, "post" => post_params}) do
+ post = Blog.get_post!(id)
+
+ case Blog.update_post(post, post_params) do
+ {:ok, post} ->
+ conn
+ |> put_flash(:info, "Post updated successfully.")
+ |> redirect(to: Routes.post_path(conn, :show, post))
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, "edit.html", post: post, changeset: changeset)
+ end
+ end
+
+ def delete(conn, %{"id" => id}) do
+ post = Blog.get_post!(id)
+ {:ok, _post} = Blog.delete_post(post)
+
+ conn
+ |> put_flash(:info, "Post deleted successfully.")
+ |> redirect(to: Routes.post_path(conn, :index))
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/endpoint.ex b/integration_test/temple_demo/lib/temple_demo_web/endpoint.ex
new file mode 100644
index 0000000..0df54eb
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/endpoint.ex
@@ -0,0 +1,59 @@
+defmodule TempleDemoWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :temple_demo
+
+ if Application.get_env(:temple_demo, :sql_sandbox) do
+ plug Phoenix.Ecto.SQL.Sandbox
+ end
+
+
+ # The session will be stored in the cookie and signed,
+ # this means its contents can be read but not tampered with.
+ # Set :encryption_salt if you would also like to encrypt it.
+ @session_options [
+ store: :cookie,
+ key: "_temple_demo_key",
+ signing_salt: "p72rbvlQ"
+ ]
+
+ socket "/socket", TempleDemoWeb.UserSocket,
+ websocket: true,
+ longpoll: false
+
+ socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # You should set gzip to true if you are running phx.digest
+ # when deploying your static files in production.
+ plug Plug.Static,
+ at: "/",
+ from: :temple_demo,
+ gzip: false,
+ only: ~w(css fonts images js favicon.ico robots.txt)
+
+ # Code reloading can be explicitly enabled under the
+ # :code_reloader configuration of your endpoint.
+ if code_reloading? do
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ plug Phoenix.Ecto.CheckRepoStatus, otp_app: :temple_demo
+ end
+
+ plug Phoenix.LiveDashboard.RequestLogger,
+ param_key: "request_logger",
+ cookie_key: "request_logger"
+
+ plug Plug.RequestId
+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+ plug Plug.Session, @session_options
+ plug TempleDemoWeb.Router
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/gettext.ex b/integration_test/temple_demo/lib/temple_demo_web/gettext.ex
new file mode 100644
index 0000000..a0aca6f
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/gettext.ex
@@ -0,0 +1,24 @@
+defmodule TempleDemoWeb.Gettext do
+ @moduledoc """
+ A module providing Internationalization with a gettext-based API.
+
+ By using [Gettext](https://hexdocs.pm/gettext),
+ your module gains a set of macros for translations, for example:
+
+ import TempleDemoWeb.Gettext
+
+ # Simple translation
+ gettext("Here is the string to translate")
+
+ # Plural translation
+ ngettext("Here is the string to translate",
+ "Here are the strings to translate",
+ 3)
+
+ # Domain-based translation
+ dgettext("errors", "Here is the error message to translate")
+
+ See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
+ """
+ use Gettext, otp_app: :temple_demo
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/router.ex b/integration_test/temple_demo/lib/temple_demo_web/router.ex
new file mode 100644
index 0000000..3f3027b
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/router.ex
@@ -0,0 +1,43 @@
+defmodule TempleDemoWeb.Router do
+ use TempleDemoWeb, :router
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_flash
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", TempleDemoWeb do
+ pipe_through :browser
+
+ get "/", PageController, :index
+ resources "/posts", PostController
+ end
+
+ # Other scopes may use custom stacks.
+ # scope "/api", TempleDemoWeb do
+ # pipe_through :api
+ # end
+
+ # Enables LiveDashboard only for development
+ #
+ # If you want to use the LiveDashboard in production, you should put
+ # it behind authentication and allow only admins to access it.
+ # If your application does not have an admins-only section yet,
+ # you can use Plug.BasicAuth to set up some basic authentication
+ # as long as you are also using SSL (which you should anyway).
+ if Mix.env() in [:dev, :test] do
+ import Phoenix.LiveDashboard.Router
+
+ scope "/" do
+ pipe_through :browser
+ live_dashboard "/dashboard", metrics: TempleDemoWeb.Telemetry
+ end
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/telemetry.ex b/integration_test/temple_demo/lib/temple_demo_web/telemetry.ex
new file mode 100644
index 0000000..ed592c4
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/telemetry.ex
@@ -0,0 +1,53 @@
+defmodule TempleDemoWeb.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+
+ # Database Metrics
+ summary("temple_demo.repo.query.total_time", unit: {:native, :millisecond}),
+ summary("temple_demo.repo.query.decode_time", unit: {:native, :millisecond}),
+ summary("temple_demo.repo.query.query_time", unit: {:native, :millisecond}),
+ summary("temple_demo.repo.query.queue_time", unit: {:native, :millisecond}),
+ summary("temple_demo.repo.query.idle_time", unit: {:native, :millisecond}),
+
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {TempleDemoWeb, :count_users, []}
+ ]
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/layout/app.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/layout/app.html.exs
new file mode 100644
index 0000000..b730bff
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/templates/layout/app.html.exs
@@ -0,0 +1,41 @@
+html lang: "en" do
+ head do
+ meta charset: "utf-8"
+ meta http_equiv: "X-UA-Compatible", content: "IE=edge"
+ meta name: "viewport", content: "width=device-width, initial-scale=1.0"
+ title do: "TempleDemo · Phoenix Framework"
+
+ _link(rel: "stylesheet", href: Routes.static_path(@conn, "/css/app.css"))
+ end
+
+ body do
+ header do
+ section class: "container" do
+ nav role: "navigation" do
+ ul do
+ li do
+ a href: "https://hexdocs.pm/phoenix/overview.html" do
+ "Get Started"
+ end
+ end
+ end
+ end
+
+ a href: "http://phoenixframework.org/", class: "phx-logo" do
+ img src: Routes.static_path(@conn, "/images/phoenix.png"),
+ alt: "Phoenix Framework Logo"
+ end
+ end
+ end
+
+ main role: "main", class: "container" do
+ p class: "alert alert-info", role: "alert", compact: true, do: get_flash(@conn, :info)
+ p class: "alert alert-danger", role: "alert", compact: true, do: get_flash(@conn, :error)
+
+ @inner_content
+ end
+
+ script type: "text/javascript", src: Routes.static_path(@conn, "/js/phoenix_html.js")
+ script type: "text/javascript", src: Routes.static_path(@conn, "/js/app.js")
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/page/index.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/page/index.html.exs
new file mode 100644
index 0000000..938d927
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/templates/page/index.html.exs
@@ -0,0 +1,65 @@
+section class: "phx-hero" do
+ h1 do
+ gettext("Welcome to %{name}!", name: "Phoenix")
+ end
+
+ p do
+ "Peace-of-mind from prototype to production"
+ end
+end
+
+section class: "row" do
+ article class: "column" do
+ h2 do: "Resources"
+
+ ul do
+ li do
+ a href: "https://hexdocs.pm/phoenix/overview.html" do
+ "Guides & Docs"
+ end
+ end
+
+ li do
+ a href: "https://github.com/phoenixframework/phoenix" do
+ "Source"
+ end
+ end
+
+ li do
+ a href: "https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md" do
+ "v1.5 Changelog"
+ end
+ end
+ end
+ end
+
+ article class: "column" do
+ h2 do: "Help"
+
+ ul do
+ li do
+ a href: "https://elixirforum.com/c/phoenix-forum" do
+ "Forum"
+ end
+ end
+
+ li do
+ a href: "https://webchat.freenode.net/?channels=elixir-lang" do
+ "#elixir-lang on Freenode IRC"
+ end
+ end
+
+ li do
+ a href: "https://twitter.com/elixirphoenix" do
+ "Twitter @elixirphoenix"
+ end
+ end
+
+ li do
+ a href: "https://elixir-slackin.herokuapp.com/" do
+ "Elixir on Slack"
+ end
+ end
+ end
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/edit.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/edit.html.exs
new file mode 100644
index 0000000..16e842f
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/templates/post/edit.html.exs
@@ -0,0 +1,7 @@
+h1 do: "Edit Post"
+
+render("form.html", Map.put(assigns, :action, Routes.post_path(@conn, :update, @post)))
+
+span do
+ link "Back", to: Routes.post_path(@conn, :index)
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/form.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/form.html.exs
new file mode 100644
index 0000000..b4d46ba
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/templates/post/form.html.exs
@@ -0,0 +1,27 @@
+form_for @changeset, @action, fn f ->
+ if @changeset.action do
+ div class: "alert alert-danger" do
+ p do: "Oops, something went wrong! Please check the errors below."
+ end
+ end
+
+ label f, :title
+ text_input f, :title
+ error_tag(f, :title)
+
+ label f, :body
+ textarea f, :body
+ error_tag(f, :body)
+
+ label f, :published_at
+ datetime_select f, :published_at
+ error_tag(f, :published_at)
+
+ label f, :author
+ text_input f, :author
+ error_tag(f, :author)
+
+ div do
+ submit "Save"
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/index.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/index.html.exs
new file mode 100644
index 0000000..01c250d
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/templates/post/index.html.exs
@@ -0,0 +1,33 @@
+h1 do: "Listing Posts"
+
+table do
+ thead do
+ tr do
+ th do: "Title"
+ th do: "Body"
+ th do: "Published at"
+ th do: "Author"
+ th()
+ end
+ tbody do
+ for post <- @posts do
+ tr do
+ td do: post.title
+ td do: post.body
+ td do: post.published_at
+ td do: post.author
+ td do
+ link "Show", to: Routes.post_path(@conn, :show, post)
+ link "Edit", to: Routes.post_path(@conn, :edit, post)
+ link "Delete", to: Routes.post_path(@conn, :delete, post),
+ method: :delete, data: [confirm: "Are you sure?"]
+ end
+ end
+ end
+ end
+ end
+end
+
+span do
+ link "New Post", to: Routes.post_path(@conn, :new)
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/new.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/new.html.exs
new file mode 100644
index 0000000..ce06483
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/templates/post/new.html.exs
@@ -0,0 +1,7 @@
+h1 do: "New Post"
+
+render("form.html", Map.put(assigns, :action, Routes.post_path(@conn, :create)))
+
+span do
+ link "Back", to: Routes.post_path(@conn, :index)
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/templates/post/show.html.exs b/integration_test/temple_demo/lib/temple_demo_web/templates/post/show.html.exs
new file mode 100644
index 0000000..a0faae1
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/templates/post/show.html.exs
@@ -0,0 +1,25 @@
+h1 do: "Show Post"
+
+ul do
+ li do: [strong(do: "Title"), @post.title]
+ li do
+ strong do: "Body"
+ Phoenix.HTML.Format.text_to_html @post.body, attributes: [class: "whitespace-pre"]
+ end
+ li do
+ strong do: "Published at"
+ @post.published_at
+ end
+ li do
+ strong do: "Author"
+ @post.author
+ end
+
+ span do
+ link "Edit", to: Routes.post_path(@conn, :edit, @post)
+ end
+
+ span do
+ link "Back", to: Routes.post_path(@conn, :index)
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/error_helpers.ex b/integration_test/temple_demo/lib/temple_demo_web/views/error_helpers.ex
new file mode 100644
index 0000000..dc48246
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/views/error_helpers.ex
@@ -0,0 +1,47 @@
+defmodule TempleDemoWeb.ErrorHelpers do
+ @moduledoc """
+ Conveniences for translating and building error messages.
+ """
+
+ use Phoenix.HTML
+
+ @doc """
+ Generates tag for inlined form input errors.
+ """
+ def error_tag(form, field) do
+ Enum.map(Keyword.get_values(form.errors, field), fn error ->
+ content_tag(:span, translate_error(error),
+ class: "invalid-feedback",
+ phx_feedback_for: input_id(form, field)
+ )
+ end)
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # When using gettext, we typically pass the strings we want
+ # to translate as a static argument:
+ #
+ # # Translate "is invalid" in the "errors" domain
+ # dgettext("errors", "is invalid")
+ #
+ # # Translate the number of files with plural rules
+ # dngettext("errors", "1 file", "%{count} files", count)
+ #
+ # Because the error messages we show in our forms and APIs
+ # are defined inside Ecto, we need to translate them dynamically.
+ # This requires us to call the Gettext module passing our gettext
+ # backend as first argument.
+ #
+ # Note we use the "errors" domain, which means translations
+ # should be written to the errors.po file. The :count option is
+ # set by Ecto and indicates we should also apply plural rules.
+ if count = opts[:count] do
+ Gettext.dngettext(TempleDemoWeb.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(TempleDemoWeb.Gettext, "errors", msg, opts)
+ end
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/error_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/error_view.ex
new file mode 100644
index 0000000..31bd467
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/views/error_view.ex
@@ -0,0 +1,16 @@
+defmodule TempleDemoWeb.ErrorView do
+ use TempleDemoWeb, :view
+
+ # If you want to customize a particular status code
+ # for a certain format, you may uncomment below.
+ # def render("500.html", _assigns) do
+ # "Internal Server Error"
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def template_not_found(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/layout_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/layout_view.ex
new file mode 100644
index 0000000..63b7f02
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/views/layout_view.ex
@@ -0,0 +1,3 @@
+defmodule TempleDemoWeb.LayoutView do
+ use TempleDemoWeb, :view
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/page_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/page_view.ex
new file mode 100644
index 0000000..4194599
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/views/page_view.ex
@@ -0,0 +1,3 @@
+defmodule TempleDemoWeb.PageView do
+ use TempleDemoWeb, :view
+end
diff --git a/integration_test/temple_demo/lib/temple_demo_web/views/post_view.ex b/integration_test/temple_demo/lib/temple_demo_web/views/post_view.ex
new file mode 100644
index 0000000..c7a92a9
--- /dev/null
+++ b/integration_test/temple_demo/lib/temple_demo_web/views/post_view.ex
@@ -0,0 +1,3 @@
+defmodule TempleDemoWeb.PostView do
+ use TempleDemoWeb, :view
+end
diff --git a/integration_test/temple_demo/mix.exs b/integration_test/temple_demo/mix.exs
new file mode 100644
index 0000000..1cbdea2
--- /dev/null
+++ b/integration_test/temple_demo/mix.exs
@@ -0,0 +1,68 @@
+defmodule TempleDemo.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :temple_demo,
+ version: "0.1.0",
+ elixir: "~> 1.7",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ compilers: [:phoenix, :gettext] ++ Mix.compilers(),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps()
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {TempleDemo.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ {:phoenix, "~> 1.5.1"},
+ {:phoenix_ecto, "~> 4.1"},
+ {:ecto_sql, "~> 3.4"},
+ {:postgrex, ">= 0.0.0"},
+ {:phoenix_html, "~> 2.11"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_live_dashboard, "~> 0.2.0"},
+ {:telemetry_metrics, "~> 0.4"},
+ {:telemetry_poller, "~> 0.4"},
+ {:gettext, "~> 0.11"},
+ {:jason, "~> 1.0"},
+ {:plug_cowboy, "~> 2.0"},
+ {:wallaby, "~> 0.26.0", only: :test},
+ {:tzdata, "~> 1.0.3"},
+ {:temple, path: "../../"}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ # For example, to install project dependencies and perform other setup tasks, run:
+ #
+ # $ mix setup
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get", "ecto.setup"],
+ "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+ "ecto.reset": ["ecto.drop", "ecto.setup"],
+ test: ["ecto.create --quiet", "ecto.migrate", "test"]
+ ]
+ end
+end
diff --git a/integration_test/temple_demo/mix.lock b/integration_test/temple_demo/mix.lock
new file mode 100644
index 0000000..92c5341
--- /dev/null
+++ b/integration_test/temple_demo/mix.lock
@@ -0,0 +1,42 @@
+%{
+ "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
+ "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
+ "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
+ "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
+ "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
+ "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
+ "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"},
+ "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
+ "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
+ "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
+ "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
+ "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"},
+ "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
+ "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
+ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
+ "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
+ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
+ "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
+ "phoenix": {:hex, :phoenix, "1.5.2", "7ba05d6cb0024eefd3cb08b176e6f041a9edff094912de2f6a49e3ba67140fb3", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3047022367d415a935dceda1176e67d9c7f2d41cd52a0419b53cfca66fc4c64e"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
+ "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.4", "3080e8a89bab3ec08d4dd9a6858dfa24af9334464aae78c83e58a2db37c6f983", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.12.0 or ~> 0.13.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1c89595ef60f1b76ac07705e73f001823af451491792a4b0d5b2b2a3789b0a00"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.13.0", "dec006b3da4ab164283d5bebe960724eb4d19cd0ed553e05fb99b260233e200f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "bd6f13b666fa9bfeca88b013db20414c693d5a5e6d19b1fc2602c282d626ed8e"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
+ "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
+ "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
+ "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
+ "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
+ "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
+ "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"},
+ "telemetry_poller": {:hex, :telemetry_poller, "0.5.0", "4770888ef85599ead39c7f51d6b4b62306e602d96c69b2625d54dea3d9a5204b", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69e4e8e65b0ae077c9e14cd5f42c7cc486de0e07ac6e3409e6f0e52699a7872c"},
+ "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"},
+ "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
+ "wallaby": {:hex, :wallaby, "0.26.0", "170b05b2fe572ec38071dbe45a908123959d5245f389f657e9a79eb463dc0431", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:web_driver_client, "~> 0.1.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "07a437e75c9276900288e4fe5c1814a5486f10a54940aa524ea65ce22b40c182"},
+ "web_driver_client": {:hex, :web_driver_client, "0.1.0", "19466a989c76b7ec803c796cec0fec4611a64f445fd5120ce50c9e3817e09c2c", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "c9c031ca915e8fc75b5e24ac93503244f3cc406dd7f53047087a45aa62d60e9e"},
+}
diff --git a/integration_test/temple_demo/priv/gettext/en/LC_MESSAGES/errors.po b/integration_test/temple_demo/priv/gettext/en/LC_MESSAGES/errors.po
new file mode 100644
index 0000000..a589998
--- /dev/null
+++ b/integration_test/temple_demo/priv/gettext/en/LC_MESSAGES/errors.po
@@ -0,0 +1,97 @@
+## `msgid`s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove `msgid`s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use `mix gettext.extract --merge` or `mix gettext.merge`
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en\n"
+
+## From Ecto.Changeset.cast/4
+msgid "can't be blank"
+msgstr ""
+
+## From Ecto.Changeset.unique_constraint/3
+msgid "has already been taken"
+msgstr ""
+
+## From Ecto.Changeset.put_change/3
+msgid "is invalid"
+msgstr ""
+
+## From Ecto.Changeset.validate_acceptance/3
+msgid "must be accepted"
+msgstr ""
+
+## From Ecto.Changeset.validate_format/3
+msgid "has invalid format"
+msgstr ""
+
+## From Ecto.Changeset.validate_subset/3
+msgid "has an invalid entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_exclusion/3
+msgid "is reserved"
+msgstr ""
+
+## From Ecto.Changeset.validate_confirmation/3
+msgid "does not match confirmation"
+msgstr ""
+
+## From Ecto.Changeset.no_assoc_constraint/3
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_length/3
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+## From Ecto.Changeset.validate_number/3
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""
diff --git a/integration_test/temple_demo/priv/gettext/errors.pot b/integration_test/temple_demo/priv/gettext/errors.pot
new file mode 100644
index 0000000..39a220b
--- /dev/null
+++ b/integration_test/temple_demo/priv/gettext/errors.pot
@@ -0,0 +1,95 @@
+## This is a PO Template file.
+##
+## `msgid`s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run `mix gettext.extract` to bring this file up to
+## date. Leave `msgstr`s empty as changing them here has no
+## effect: edit them in PO (`.po`) files instead.
+
+## From Ecto.Changeset.cast/4
+msgid "can't be blank"
+msgstr ""
+
+## From Ecto.Changeset.unique_constraint/3
+msgid "has already been taken"
+msgstr ""
+
+## From Ecto.Changeset.put_change/3
+msgid "is invalid"
+msgstr ""
+
+## From Ecto.Changeset.validate_acceptance/3
+msgid "must be accepted"
+msgstr ""
+
+## From Ecto.Changeset.validate_format/3
+msgid "has invalid format"
+msgstr ""
+
+## From Ecto.Changeset.validate_subset/3
+msgid "has an invalid entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_exclusion/3
+msgid "is reserved"
+msgstr ""
+
+## From Ecto.Changeset.validate_confirmation/3
+msgid "does not match confirmation"
+msgstr ""
+
+## From Ecto.Changeset.no_assoc_constraint/3
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_length/3
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+## From Ecto.Changeset.validate_number/3
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""
diff --git a/integration_test/temple_demo/priv/repo/migrations/.formatter.exs b/integration_test/temple_demo/priv/repo/migrations/.formatter.exs
new file mode 100644
index 0000000..49f9151
--- /dev/null
+++ b/integration_test/temple_demo/priv/repo/migrations/.formatter.exs
@@ -0,0 +1,4 @@
+[
+ import_deps: [:ecto_sql],
+ inputs: ["*.exs"]
+]
diff --git a/integration_test/temple_demo/priv/repo/migrations/20200522023854_create_posts.exs b/integration_test/temple_demo/priv/repo/migrations/20200522023854_create_posts.exs
new file mode 100644
index 0000000..b2b3c3a
--- /dev/null
+++ b/integration_test/temple_demo/priv/repo/migrations/20200522023854_create_posts.exs
@@ -0,0 +1,15 @@
+defmodule TempleDemo.Repo.Migrations.CreatePosts do
+ use Ecto.Migration
+
+ def change do
+ create table(:posts) do
+ add :title, :string
+ add :body, :text
+ add :published_at, :naive_datetime
+ add :author, :string
+
+ timestamps()
+ end
+
+ end
+end
diff --git a/integration_test/temple_demo/priv/repo/seeds.exs b/integration_test/temple_demo/priv/repo/seeds.exs
new file mode 100644
index 0000000..04ba8d9
--- /dev/null
+++ b/integration_test/temple_demo/priv/repo/seeds.exs
@@ -0,0 +1,11 @@
+# Script for populating the database. You can run it as:
+#
+# mix run priv/repo/seeds.exs
+#
+# Inside the script, you can read and write to any of your
+# repositories directly:
+#
+# TempleDemo.Repo.insert!(%TempleDemo.SomeSchema{})
+#
+# We recommend using the bang functions (`insert!`, `update!`
+# and so on) as they will fail if something goes wrong.
diff --git a/integration_test/temple_demo/priv/static/css/app.css b/integration_test/temple_demo/priv/static/css/app.css
new file mode 100644
index 0000000..861994c
--- /dev/null
+++ b/integration_test/temple_demo/priv/static/css/app.css
@@ -0,0 +1,40 @@
+/* This file is for your main application css. */
+@import "./phoenix.css";
+
+.invalid-feedback {
+ color: red;
+}
+
+.alert:empty {
+ display: none;
+}
+
+.alert {
+ padding: 15px;
+ margin-bottom: 20px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+}
+
+.alert-info {
+ color: #31708f;
+ background-color: #d9edf7;
+ border-color: #bce8f1;
+}
+.alert-warning {
+ color: #8a6d3b;
+ background-color: #fcf8e3;
+ border-color: #faebcc;
+}
+.alert-danger {
+ color: #a94442;
+ background-color: #f2dede;
+ border-color: #ebccd1;
+}
+.alert p {
+ margin-bottom: 0;
+}
+
+strong {
+ font-weight: 700;
+}
diff --git a/integration_test/temple_demo/priv/static/css/phoenix.css b/integration_test/temple_demo/priv/static/css/phoenix.css
new file mode 100644
index 0000000..3767b31
--- /dev/null
+++ b/integration_test/temple_demo/priv/static/css/phoenix.css
@@ -0,0 +1,101 @@
+/* Includes some default style for the starter application.
+ * This can be safely deleted to start fresh.
+ */
+
+/* Milligram v1.3.0 https://milligram.github.io
+ * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license
+ */
+
+*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}
+
+/* General style */
+h1{font-size: 3.6rem; line-height: 1.25}
+h2{font-size: 2.8rem; line-height: 1.3}
+h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35}
+h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5}
+h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4}
+h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2}
+pre{padding: 1em;}
+
+.container{
+ margin: 0 auto;
+ max-width: 80.0rem;
+ padding: 0 2.0rem;
+ position: relative;
+ width: 100%
+}
+select {
+ width: auto;
+}
+
+/* Phoenix promo and logo */
+.phx-hero {
+ text-align: center;
+ border-bottom: 1px solid #e3e3e3;
+ background: #eee;
+ border-radius: 6px;
+ padding: 3em 3em 1em;
+ margin-bottom: 3rem;
+ font-weight: 200;
+ font-size: 120%;
+}
+.phx-hero input {
+ background: #ffffff;
+}
+.phx-logo {
+ min-width: 300px;
+ margin: 1rem;
+ display: block;
+}
+.phx-logo img {
+ width: auto;
+ display: block;
+}
+
+/* Headers */
+header {
+ width: 100%;
+ background: #fdfdfd;
+ border-bottom: 1px solid #eaeaea;
+ margin-bottom: 2rem;
+}
+header section {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+header section :first-child {
+ order: 2;
+}
+header section :last-child {
+ order: 1;
+}
+header nav ul,
+header nav li {
+ margin: 0;
+ padding: 0;
+ display: block;
+ text-align: right;
+ white-space: nowrap;
+}
+header nav ul {
+ margin: 1rem;
+ margin-top: 0;
+}
+header nav a {
+ display: block;
+}
+
+@media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */
+ header section {
+ flex-direction: row;
+ }
+ header nav ul {
+ margin: 1rem;
+ }
+ .phx-logo {
+ flex-basis: 527px;
+ margin: 2rem 1rem;
+ }
+}
diff --git a/integration_test/temple_demo/priv/static/favicon.ico b/integration_test/temple_demo/priv/static/favicon.ico
new file mode 100644
index 0000000..73de524
Binary files /dev/null and b/integration_test/temple_demo/priv/static/favicon.ico differ
diff --git a/integration_test/temple_demo/priv/static/images/phoenix.png b/integration_test/temple_demo/priv/static/images/phoenix.png
new file mode 100644
index 0000000..9c81075
Binary files /dev/null and b/integration_test/temple_demo/priv/static/images/phoenix.png differ
diff --git a/integration_test/temple_demo/priv/static/js/app.js b/integration_test/temple_demo/priv/static/js/app.js
new file mode 100644
index 0000000..c0b39de
--- /dev/null
+++ b/integration_test/temple_demo/priv/static/js/app.js
@@ -0,0 +1,3 @@
+// for phoenix_html support, including form and button helpers
+// copy the following scripts into your javascript bundle:
+// * deps/phoenix_html/priv/static/phoenix_html.js
diff --git a/integration_test/temple_demo/priv/static/js/phoenix.js b/integration_test/temple_demo/priv/static/js/phoenix.js
new file mode 100644
index 0000000..692f637
--- /dev/null
+++ b/integration_test/temple_demo/priv/static/js/phoenix.js
@@ -0,0 +1 @@
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){(function(t){e.exports=t.Phoenix=n(2)}).call(this,n(1))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";function i(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}(e)||s(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var n=[],i=!0,o=!1,r=void 0;try{for(var s,a=e[Symbol.iterator]();!(i=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);i=!0);}catch(e){o=!0,r=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw r}}return n}(e,t)||s(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(n):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,i=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:this.timeout;if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}},{key:"onClose",value:function(e){this.on(R,e)}},{key:"onError",value:function(e){return this.on(S,(function(t){return e(t)}))}},{key:"on",value:function(e,t){var n=this.bindingRef++;return this.bindings.push({event:e,ref:n,callback:t}),n}},{key:"off",value:function(e,t){this.bindings=this.bindings.filter((function(n){return!(n.event===e&&(void 0===t||t===n.ref))}))}},{key:"canPush",value:function(){return this.socket.isConnected()&&this.isJoined()}},{key:"push",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.timeout;if(!this.joinedOnce)throw new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new _(this,e,(function(){return t}),n);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}},{key:"leave",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=C;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(R,"leave")},i=new _(this,E,L({}),t);return i.receive("ok",(function(){return n()})).receive("timeout",(function(){return n()})),i.send(),this.canPush()||i.trigger("ok",{}),i}},{key:"onMessage",value:function(e,t,n){return t}},{key:"isLifecycleEvent",value:function(e){return x.indexOf(e)>=0}},{key:"isMember",value:function(e,t,n,i){return this.topic===e&&(!i||i===this.joinRef()||!this.isLifecycleEvent(t)||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:n,joinRef:i}),!1))}},{key:"joinRef",value:function(){return this.joinPush.ref}},{key:"rejoin",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=j,this.joinPush.resend(e))}},{key:"trigger",value:function(e,t,n,i){var o=this.onMessage(e,t,n,i);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");for(var r=this.bindings.filter((function(t){return t.event===e})),s=0;s1&&void 0!==arguments[1]?arguments[1]:{};c(this,e),this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=i.timeout||1e4,this.transport=i.transport||d.WebSocket||H,this.defaultEncoder=M.encode,this.defaultDecoder=M.decode,this.closeWasClean=!1,this.unloaded=!1,this.binaryType=i.binaryType||"arraybuffer",this.transport!==H?(this.encode=i.encode||this.defaultEncoder,this.decode=i.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),f&&f.addEventListener&&f.addEventListener("unload",(function(e){n.conn&&(n.unloaded=!0,n.abnormalClose("unloaded"))})),this.heartbeatIntervalMs=i.heartbeatIntervalMs||3e4,this.rejoinAfterMs=function(e){return i.rejoinAfterMs?i.rejoinAfterMs(e):[1e3,2e3,5e3][e-1]||1e4},this.reconnectAfterMs=function(e){return n.unloaded?100:i.reconnectAfterMs?i.reconnectAfterMs(e):[10,50,100,150,200,250,500,1e3,2e3][e-1]||5e3},this.logger=i.logger||null,this.longpollerTimeout=i.longpollerTimeout||2e4,this.params=L(i.params||{}),this.endPoint="".concat(t,"/").concat(P),this.vsn=i.vsn||"2.0.0",this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new I((function(){n.teardown((function(){return n.connect()}))}),this.reconnectAfterMs)}return h(e,[{key:"protocol",value:function(){return location.protocol.match(/^https/)?"wss":"ws"}},{key:"endPointURL",value:function(){var e=D.appendParams(D.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?"".concat(this.protocol(),":").concat(e):"".concat(this.protocol(),"://").concat(location.host).concat(e)}},{key:"disconnect",value:function(e,t,n){this.closeWasClean=!0,this.reconnectTimer.reset(),this.teardown(e,t,n)}},{key:"connect",value:function(e){var t=this;e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=L(e)),this.conn||(this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=function(){return t.onConnOpen()},this.conn.onerror=function(e){return t.onConnError(e)},this.conn.onmessage=function(e){return t.onConnMessage(e)},this.conn.onclose=function(e){return t.onConnClose(e)})}},{key:"log",value:function(e,t,n){this.logger(e,t,n)}},{key:"hasLogger",value:function(){return null!==this.logger}},{key:"onOpen",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}},{key:"onClose",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}},{key:"onError",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}},{key:"onMessage",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}},{key:"onConnOpen",value:function(){this.hasLogger()&&this.log("transport","connected to ".concat(this.endPointURL())),this.unloaded=!1,this.closeWasClean=!1,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach((function(e){return(0,r(e,2)[1])()}))}},{key:"resetHeartbeat",value:function(){var e=this;this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval((function(){return e.sendHeartbeat()}),this.heartbeatIntervalMs))}},{key:"teardown",value:function(e,t,n){var i=this;if(!this.conn)return e&&e();this.waitForBufferDone((function(){i.conn&&(t?i.conn.close(t,n||""):i.conn.close()),i.waitForSocketClosed((function(){i.conn&&(i.conn.onclose=function(){},i.conn=null),e&&e()}))}))}},{key:"waitForBufferDone",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5===n||!this.conn||this.conn.bufferedAmount&&0===this.conn.bufferedAmount?e():setTimeout((function(){t.waitForBufferDone(e,n+1)}),150*n)}},{key:"waitForSocketClosed",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.readyState!==m?setTimeout((function(){t.waitForSocketClosed(e,n+1)}),150*n):e()}},{key:"onConnClose",value:function(e){this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),this.closeWasClean||this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"onConnError",value:function(e){this.hasLogger()&&this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"triggerChanError",value:function(){this.channels.forEach((function(e){e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(S)}))}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case p:return"connecting";case v:return"open";case y:return"closing";default:return"closed"}}},{key:"isConnected",value:function(){return"open"===this.connectionState()}},{key:"remove",value:function(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter((function(t){return t.joinRef()!==e.joinRef()}))}},{key:"off",value:function(e){for(var t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter((function(t){var n=r(t,1)[0];return-1===e.indexOf(n)}))}},{key:"channel",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new A(e,t,this);return this.channels.push(n),n}},{key:"push",value:function(e){var t=this;if(this.hasLogger()){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;this.log("push","".concat(n," ").concat(i," (").concat(s,", ").concat(r,")"),o)}this.isConnected()?this.encode(e,(function(e){return t.conn.send(e)})):this.sendBuffer.push((function(){return t.encode(e,(function(e){return t.conn.send(e)}))}))}},{key:"makeRef",value:function(){var e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}},{key:"sendHeartbeat",value:function(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.abnormalClose("heartbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}},{key:"abnormalClose",value:function(e){this.closeWasClean=!1,this.conn.close(1e3,e)}},{key:"flushSendBuffer",value:function(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach((function(e){return e()})),this.sendBuffer=[])}},{key:"onConnMessage",value:function(e){var t=this;this.decode(e.data,(function(e){var n=e.topic,i=e.event,o=e.payload,s=e.ref,a=e.join_ref;s&&s===t.pendingHeartbeatRef&&(t.pendingHeartbeatRef=null),t.hasLogger()&&t.log("receive","".concat(o.status||""," ").concat(n," ").concat(i," ").concat(s&&"("+s+")"||""),o);for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:{};c(this,e);var o=i.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=t,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(o.state,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.joinRef=n.channel.joinRef(),n.state=e.syncState(n.state,t,o,r),n.pendingDiffs.forEach((function(t){n.state=e.syncDiff(n.state,t,o,r)})),n.pendingDiffs=[],s()})),this.channel.on(o.diff,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.inPendingSyncState()?n.pendingDiffs.push(t):(n.state=e.syncDiff(n.state,t,o,r),s())}))}return h(e,[{key:"onJoin",value:function(e){this.caller.onJoin=e}},{key:"onLeave",value:function(e){this.caller.onLeave=e}},{key:"onSync",value:function(e){this.caller.onSync=e}},{key:"list",value:function(t){return e.list(this.state,t)}},{key:"inPendingSyncState",value:function(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}}],[{key:"syncState",value:function(e,t,n,i){var o=this,r=this.clone(e),s={},a={};return this.map(r,(function(e,n){t[e]||(a[e]=n)})),this.map(t,(function(e,t){var n=r[e];if(n){var i=t.metas.map((function(e){return e.phx_ref})),c=n.metas.map((function(e){return e.phx_ref})),u=t.metas.filter((function(e){return c.indexOf(e.phx_ref)<0})),h=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0}));u.length>0&&(s[e]=t,s[e].metas=u),h.length>0&&(a[e]=o.clone(n),a[e].metas=h)}else s[e]=t})),this.syncDiff(r,{joins:s,leaves:a},n,i)}},{key:"syncDiff",value:function(e,t,n,o){var r=t.joins,s=t.leaves,a=this.clone(e);return n||(n=function(){}),o||(o=function(){}),this.map(r,(function(e,t){var o=a[e];if(a[e]=t,o){var r,s=a[e].metas.map((function(e){return e.phx_ref})),c=o.metas.filter((function(e){return s.indexOf(e.phx_ref)<0}));(r=a[e].metas).unshift.apply(r,i(c))}n(e,o,t)})),this.map(s,(function(e,t){var n=a[e];if(n){var i=t.metas.map((function(e){return e.phx_ref}));n.metas=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0})),o(e,n,t),0===n.metas.length&&delete a[e]}})),a}},{key:"list",value:function(e,t){return t||(t=function(e,t){return t}),this.map(e,(function(e,n){return t(e,n)}))}},{key:"map",value:function(e,t){return Object.getOwnPropertyNames(e).map((function(n){return t(n,e[n])}))}},{key:"clone",value:function(e){return JSON.parse(JSON.stringify(e))}}]),e}(),I=function(){function e(t,n){c(this,e),this.callback=t,this.timerCalc=n,this.timer=null,this.tries=0}return h(e,[{key:"reset",value:function(){this.tries=0,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout((function(){e.tries=e.tries+1,e.callback()}),this.timerCalc(this.tries+1))}}]),e}()}])}));
\ No newline at end of file
diff --git a/integration_test/temple_demo/priv/static/js/phoenix_html.js b/integration_test/temple_demo/priv/static/js/phoenix_html.js
new file mode 100644
index 0000000..e1ae852
--- /dev/null
+++ b/integration_test/temple_demo/priv/static/js/phoenix_html.js
@@ -0,0 +1,76 @@
+"use strict";
+
+(function() {
+ var PolyfillEvent = eventConstructor();
+
+ function eventConstructor() {
+ if (typeof window.CustomEvent === "function") return window.CustomEvent;
+ // IE<=9 Support
+ function CustomEvent(event, params) {
+ params = params || {bubbles: false, cancelable: false, detail: undefined};
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ return evt;
+ }
+ CustomEvent.prototype = window.Event.prototype;
+ return CustomEvent;
+ }
+
+ function buildHiddenInput(name, value) {
+ var input = document.createElement("input");
+ input.type = "hidden";
+ input.name = name;
+ input.value = value;
+ return input;
+ }
+
+ function handleClick(element) {
+ var to = element.getAttribute("data-to"),
+ method = buildHiddenInput("_method", element.getAttribute("data-method")),
+ csrf = buildHiddenInput("_csrf_token", element.getAttribute("data-csrf")),
+ form = document.createElement("form"),
+ target = element.getAttribute("target");
+
+ form.method = (element.getAttribute("data-method") === "get") ? "get" : "post";
+ form.action = to;
+ form.style.display = "hidden";
+
+ if (target) form.target = target;
+
+ form.appendChild(csrf);
+ form.appendChild(method);
+ document.body.appendChild(form);
+ form.submit();
+ }
+
+ window.addEventListener("click", function(e) {
+ var element = e.target;
+
+ while (element && element.getAttribute) {
+ var phoenixLinkEvent = new PolyfillEvent('phoenix.link.click', {
+ "bubbles": true, "cancelable": true
+ });
+
+ if (!element.dispatchEvent(phoenixLinkEvent)) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
+
+ if (element.getAttribute("data-method")) {
+ handleClick(element);
+ e.preventDefault();
+ return false;
+ } else {
+ element = element.parentNode;
+ }
+ }
+ }, false);
+
+ window.addEventListener('phoenix.link.click', function (e) {
+ var message = e.target.getAttribute("data-confirm");
+ if(message && !window.confirm(message)) {
+ e.preventDefault();
+ }
+ }, false);
+})();
diff --git a/integration_test/temple_demo/priv/static/robots.txt b/integration_test/temple_demo/priv/static/robots.txt
new file mode 100644
index 0000000..3c9c7c0
--- /dev/null
+++ b/integration_test/temple_demo/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/integration_test/temple_demo/test/support/channel_case.ex b/integration_test/temple_demo/test/support/channel_case.ex
new file mode 100644
index 0000000..1cbae75
--- /dev/null
+++ b/integration_test/temple_demo/test/support/channel_case.ex
@@ -0,0 +1,40 @@
+defmodule TempleDemoWeb.ChannelCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ channel tests.
+
+ Such tests rely on `Phoenix.ChannelTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use TempleDemoWeb.ChannelCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Import conveniences for testing with channels
+ import Phoenix.ChannelTest
+ import TempleDemoWeb.ChannelCase
+
+ # The default endpoint for testing
+ @endpoint TempleDemoWeb.Endpoint
+ end
+ end
+
+ setup tags do
+ :ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo)
+
+ unless tags[:async] do
+ Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()})
+ end
+
+ :ok
+ end
+end
diff --git a/integration_test/temple_demo/test/support/conn_case.ex b/integration_test/temple_demo/test/support/conn_case.ex
new file mode 100644
index 0000000..8ce6ed3
--- /dev/null
+++ b/integration_test/temple_demo/test/support/conn_case.ex
@@ -0,0 +1,43 @@
+defmodule TempleDemoWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use TempleDemoWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import TempleDemoWeb.ConnCase
+
+ alias TempleDemoWeb.Router.Helpers, as: Routes
+
+ # The default endpoint for testing
+ @endpoint TempleDemoWeb.Endpoint
+ end
+ end
+
+ setup tags do
+ :ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo)
+
+ unless tags[:async] do
+ Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()})
+ end
+
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+end
diff --git a/integration_test/temple_demo/test/support/data_case.ex b/integration_test/temple_demo/test/support/data_case.ex
new file mode 100644
index 0000000..7dfbe0e
--- /dev/null
+++ b/integration_test/temple_demo/test/support/data_case.ex
@@ -0,0 +1,55 @@
+defmodule TempleDemo.DataCase do
+ @moduledoc """
+ This module defines the setup for tests requiring
+ access to the application's data layer.
+
+ You may define functions here to be used as helpers in
+ your tests.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use TempleDemo.DataCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ alias TempleDemo.Repo
+
+ import Ecto
+ import Ecto.Changeset
+ import Ecto.Query
+ import TempleDemo.DataCase
+ end
+ end
+
+ setup tags do
+ :ok = Ecto.Adapters.SQL.Sandbox.checkout(TempleDemo.Repo)
+
+ unless tags[:async] do
+ Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, {:shared, self()})
+ end
+
+ :ok
+ end
+
+ @doc """
+ A helper that transforms changeset errors into a map of messages.
+
+ assert {:error, changeset} = Accounts.create_user(%{password: "short"})
+ assert "password is too short" in errors_on(changeset).password
+ assert %{password: ["password is too short"]} = errors_on(changeset)
+
+ """
+ def errors_on(changeset) do
+ Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
+ Regex.replace(~r"%{(\w+)}", message, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
+end
diff --git a/integration_test/temple_demo/test/temple_demo/blog_test.exs b/integration_test/temple_demo/test/temple_demo/blog_test.exs
new file mode 100644
index 0000000..2752395
--- /dev/null
+++ b/integration_test/temple_demo/test/temple_demo/blog_test.exs
@@ -0,0 +1,70 @@
+defmodule TempleDemo.BlogTest do
+ use TempleDemo.DataCase
+
+ alias TempleDemo.Blog
+
+ describe "posts" do
+ alias TempleDemo.Blog.Post
+
+ @valid_attrs %{author: "some author", body: "some body", published_at: ~N[2010-04-17 14:00:00], title: "some title"}
+ @update_attrs %{author: "some updated author", body: "some updated body", published_at: ~N[2011-05-18 15:01:01], title: "some updated title"}
+ @invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
+
+ def post_fixture(attrs \\ %{}) do
+ {:ok, post} =
+ attrs
+ |> Enum.into(@valid_attrs)
+ |> Blog.create_post()
+
+ post
+ end
+
+ test "list_posts/0 returns all posts" do
+ post = post_fixture()
+ assert Blog.list_posts() == [post]
+ end
+
+ test "get_post!/1 returns the post with given id" do
+ post = post_fixture()
+ assert Blog.get_post!(post.id) == post
+ end
+
+ test "create_post/1 with valid data creates a post" do
+ assert {:ok, %Post{} = post} = Blog.create_post(@valid_attrs)
+ assert post.author == "some author"
+ assert post.body == "some body"
+ assert post.published_at == ~N[2010-04-17 14:00:00]
+ assert post.title == "some title"
+ end
+
+ test "create_post/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Blog.create_post(@invalid_attrs)
+ end
+
+ test "update_post/2 with valid data updates the post" do
+ post = post_fixture()
+ assert {:ok, %Post{} = post} = Blog.update_post(post, @update_attrs)
+ assert post.author == "some updated author"
+ assert post.body == "some updated body"
+ assert post.published_at == ~N[2011-05-18 15:01:01]
+ assert post.title == "some updated title"
+ end
+
+ test "update_post/2 with invalid data returns error changeset" do
+ post = post_fixture()
+ assert {:error, %Ecto.Changeset{}} = Blog.update_post(post, @invalid_attrs)
+ assert post == Blog.get_post!(post.id)
+ end
+
+ test "delete_post/1 deletes the post" do
+ post = post_fixture()
+ assert {:ok, %Post{}} = Blog.delete_post(post)
+ assert_raise Ecto.NoResultsError, fn -> Blog.get_post!(post.id) end
+ end
+
+ test "change_post/1 returns a post changeset" do
+ post = post_fixture()
+ assert %Ecto.Changeset{} = Blog.change_post(post)
+ end
+ end
+end
diff --git a/integration_test/temple_demo/test/temple_demo_web/controllers/page_controller_test.exs b/integration_test/temple_demo/test/temple_demo_web/controllers/page_controller_test.exs
new file mode 100644
index 0000000..b6c60a9
--- /dev/null
+++ b/integration_test/temple_demo/test/temple_demo_web/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule TempleDemoWeb.PageControllerTest do
+ use TempleDemoWeb.ConnCase
+
+ test "GET /", %{conn: conn} do
+ conn = get(conn, "/")
+ assert html_response(conn, 200) =~ "Welcome to Phoenix!"
+ end
+end
diff --git a/integration_test/temple_demo/test/temple_demo_web/controllers/post_controller_test.exs b/integration_test/temple_demo/test/temple_demo_web/controllers/post_controller_test.exs
new file mode 100644
index 0000000..4dcad38
--- /dev/null
+++ b/integration_test/temple_demo/test/temple_demo_web/controllers/post_controller_test.exs
@@ -0,0 +1,88 @@
+defmodule TempleDemoWeb.PostControllerTest do
+ use TempleDemoWeb.ConnCase
+
+ alias TempleDemo.Blog
+
+ @create_attrs %{author: "some author", body: "some body", published_at: ~N[2010-04-17 14:00:00], title: "some title"}
+ @update_attrs %{author: "some updated author", body: "some updated body", published_at: ~N[2011-05-18 15:01:01], title: "some updated title"}
+ @invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
+
+ def fixture(:post) do
+ {:ok, post} = Blog.create_post(@create_attrs)
+ post
+ end
+
+ describe "index" do
+ test "lists all posts", %{conn: conn} do
+ conn = get(conn, Routes.post_path(conn, :index))
+ assert html_response(conn, 200) =~ "Listing Posts"
+ end
+ end
+
+ describe "new post" do
+ test "renders form", %{conn: conn} do
+ conn = get(conn, Routes.post_path(conn, :new))
+ assert html_response(conn, 200) =~ "New Post"
+ end
+ end
+
+ describe "create post" do
+ test "redirects to show when data is valid", %{conn: conn} do
+ conn = post(conn, Routes.post_path(conn, :create), post: @create_attrs)
+
+ assert %{id: id} = redirected_params(conn)
+ assert redirected_to(conn) == Routes.post_path(conn, :show, id)
+
+ conn = get(conn, Routes.post_path(conn, :show, id))
+ assert html_response(conn, 200) =~ "Show Post"
+ end
+
+ test "renders errors when data is invalid", %{conn: conn} do
+ conn = post(conn, Routes.post_path(conn, :create), post: @invalid_attrs)
+ assert html_response(conn, 200) =~ "New Post"
+ end
+ end
+
+ describe "edit post" do
+ setup [:create_post]
+
+ test "renders form for editing chosen post", %{conn: conn, post: post} do
+ conn = get(conn, Routes.post_path(conn, :edit, post))
+ assert html_response(conn, 200) =~ "Edit Post"
+ end
+ end
+
+ describe "update post" do
+ setup [:create_post]
+
+ test "redirects when data is valid", %{conn: conn, post: post} do
+ conn = put(conn, Routes.post_path(conn, :update, post), post: @update_attrs)
+ assert redirected_to(conn) == Routes.post_path(conn, :show, post)
+
+ conn = get(conn, Routes.post_path(conn, :show, post))
+ assert html_response(conn, 200) =~ "some updated author"
+ end
+
+ test "renders errors when data is invalid", %{conn: conn, post: post} do
+ conn = put(conn, Routes.post_path(conn, :update, post), post: @invalid_attrs)
+ assert html_response(conn, 200) =~ "Edit Post"
+ end
+ end
+
+ describe "delete post" do
+ setup [:create_post]
+
+ test "deletes chosen post", %{conn: conn, post: post} do
+ conn = delete(conn, Routes.post_path(conn, :delete, post))
+ assert redirected_to(conn) == Routes.post_path(conn, :index)
+ assert_error_sent 404, fn ->
+ get(conn, Routes.post_path(conn, :show, post))
+ end
+ end
+ end
+
+ defp create_post(_) do
+ post = fixture(:post)
+ {:ok, post: post}
+ end
+end
diff --git a/integration_test/temple_demo/test/temple_demo_web/features/temple_feature_test.exs b/integration_test/temple_demo/test/temple_demo_web/features/temple_feature_test.exs
new file mode 100644
index 0000000..917745a
--- /dev/null
+++ b/integration_test/temple_demo/test/temple_demo_web/features/temple_feature_test.exs
@@ -0,0 +1,32 @@
+defmodule TempleDemoWeb.TempleFeatureTest do
+ use ExUnit.Case, async: false
+ use Wallaby.Feature
+ alias TempleDemoWeb.Router.Helpers, as: Routes
+ alias TempleDemoWeb.Endpoint, as: E
+
+ feature "renders the homepage", %{session: session} do
+ session
+ |> visit("/")
+ |> assert_text("Welcome to Phoenix!")
+ end
+
+ feature "can create a new post", %{session: session} do
+ session
+ |> visit(Routes.post_path(E, :index))
+ |> click(Query.link("New Post"))
+ |> fill_in(Query.text_field("Title"), with: "Temple is awesome!")
+ |> fill_in(Query.text_field("Body"), with: "In this post I will show you how to use Temple")
+ |> find(Query.select("post_published_at_year"), fn s ->
+ s |> click(Query.option("2020"))
+ end)
+ |> find(Query.select("post_published_at_month"), fn s ->
+ s |> click(Query.option("May"))
+ end)
+ |> find(Query.select("post_published_at_day"), fn s ->
+ s |> click(Query.option("21"))
+ end)
+ |> fill_in(Query.text_field("Author"), with: "Mitchelob Ultra")
+ |> click(Query.button("Save"))
+ |> assert_text("Post created successfully.")
+ end
+end
diff --git a/integration_test/temple_demo/test/temple_demo_web/views/error_view_test.exs b/integration_test/temple_demo/test/temple_demo_web/views/error_view_test.exs
new file mode 100644
index 0000000..bda7ef1
--- /dev/null
+++ b/integration_test/temple_demo/test/temple_demo_web/views/error_view_test.exs
@@ -0,0 +1,14 @@
+defmodule TempleDemoWeb.ErrorViewTest do
+ use TempleDemoWeb.ConnCase, async: true
+
+ # Bring render/3 and render_to_string/3 for testing custom views
+ import Phoenix.View
+
+ test "renders 404.html" do
+ assert render_to_string(TempleDemoWeb.ErrorView, "404.html", []) == "Not Found"
+ end
+
+ test "renders 500.html" do
+ assert render_to_string(TempleDemoWeb.ErrorView, "500.html", []) == "Internal Server Error"
+ end
+end
diff --git a/integration_test/temple_demo/test/temple_demo_web/views/layout_view_test.exs b/integration_test/temple_demo/test/temple_demo_web/views/layout_view_test.exs
new file mode 100644
index 0000000..d84d669
--- /dev/null
+++ b/integration_test/temple_demo/test/temple_demo_web/views/layout_view_test.exs
@@ -0,0 +1,8 @@
+defmodule TempleDemoWeb.LayoutViewTest do
+ use TempleDemoWeb.ConnCase, async: true
+
+ # When testing helpers, you may want to import Phoenix.HTML and
+ # use functions such as safe_to_string() to convert the helper
+ # result into an HTML string.
+ # import Phoenix.HTML
+end
diff --git a/integration_test/temple_demo/test/temple_demo_web/views/page_view_test.exs b/integration_test/temple_demo/test/temple_demo_web/views/page_view_test.exs
new file mode 100644
index 0000000..d6e0210
--- /dev/null
+++ b/integration_test/temple_demo/test/temple_demo_web/views/page_view_test.exs
@@ -0,0 +1,3 @@
+defmodule TempleDemoWeb.PageViewTest do
+ use TempleDemoWeb.ConnCase, async: true
+end
diff --git a/integration_test/temple_demo/test/test_helper.exs b/integration_test/temple_demo/test/test_helper.exs
new file mode 100644
index 0000000..ab9695a
--- /dev/null
+++ b/integration_test/temple_demo/test/test_helper.exs
@@ -0,0 +1,3 @@
+Ecto.Adapters.SQL.Sandbox.mode(TempleDemo.Repo, :manual)
+
+ExUnit.start()
diff --git a/lib/buffer.ex b/lib/buffer.ex
new file mode 100644
index 0000000..5869fa2
--- /dev/null
+++ b/lib/buffer.ex
@@ -0,0 +1,32 @@
+defmodule Temple.Buffer do
+ @moduledoc false
+ def start_link(state \\ []) do
+ Agent.start_link(fn -> state end)
+ end
+
+ def put(buffer, value) do
+ Agent.update(buffer, fn b -> [value | b] end)
+ end
+
+ def remove_new_line(buffer) do
+ Agent.update(buffer, fn
+ ["\n" | rest] ->
+ rest
+
+ rest ->
+ rest
+ end)
+ end
+
+ def get(buffer) do
+ buffer
+ |> Agent.get(& &1)
+ |> Enum.reverse()
+ |> Enum.join()
+ |> String.trim()
+ end
+
+ def stop(buffer) do
+ Agent.stop(buffer)
+ end
+end
diff --git a/lib/mix/tasks/temple.convert.ex b/lib/mix/tasks/temple.convert.ex
deleted file mode 100644
index 9375dbc..0000000
--- a/lib/mix/tasks/temple.convert.ex
+++ /dev/null
@@ -1,23 +0,0 @@
-defmodule Mix.Tasks.Temple.Convert do
- use Mix.Task
- @preferred_cli_env :dev
- @shortdoc "Converts HTML to Temple syntax"
- @moduledoc """
- Converts HTML to Temple syntax
-
- Takes HTML from a file or from stdin and outputs temple syntax to stdout.
- """
-
- def run(args) do
- html =
- if Enum.count(args) > 0 do
- args |> List.first() |> File.read!()
- else
- IO.read(:stdio, :all)
- end
-
- {:ok, result} = Temple.HtmlToTemple.parse(html)
-
- IO.write(result)
- end
-end
diff --git a/lib/mix/tasks/temple.gen.html.ex b/lib/mix/tasks/temple.gen.html.ex
index 9576c2b..b56771f 100644
--- a/lib/mix/tasks/temple.gen.html.ex
+++ b/lib/mix/tasks/temple.gen.html.ex
@@ -213,51 +213,51 @@ if Code.ensure_loaded?(Mix.Phoenix) do
{nil, nil, nil}
{key, :integer} ->
- {label(key), ~s(number_input form, #{inspect(key)}), error(key)}
+ {label(key), ~s(number_input f, #{inspect(key)}), error(key)}
{key, :float} ->
- {label(key), ~s(number_input form, #{inspect(key)}, step: "any"), error(key)}
+ {label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)}
{key, :decimal} ->
- {label(key), ~s(number_input form, #{inspect(key)}, step: "any"), error(key)}
+ {label(key), ~s(number_input f, #{inspect(key)}, step: "any"), error(key)}
{key, :boolean} ->
- {label(key), ~s(checkbox form, #{inspect(key)}), error(key)}
+ {label(key), ~s(checkbox f, #{inspect(key)}), error(key)}
{key, :text} ->
- {label(key), ~s(textarea form, #{inspect(key)}), error(key)}
+ {label(key), ~s(textarea f, #{inspect(key)}), error(key)}
{key, :date} ->
- {label(key), ~s(date_select form, #{inspect(key)}), error(key)}
+ {label(key), ~s(date_select f, #{inspect(key)}), error(key)}
{key, :time} ->
- {label(key), ~s(time_select form, #{inspect(key)}), error(key)}
+ {label(key), ~s(time_select f, #{inspect(key)}), error(key)}
{key, :utc_datetime} ->
- {label(key), ~s(datetime_select form, #{inspect(key)}), error(key)}
+ {label(key), ~s(datetime_select f, #{inspect(key)}), error(key)}
{key, :naive_datetime} ->
- {label(key), ~s(datetime_select form, #{inspect(key)}), error(key)}
+ {label(key), ~s(datetime_select f, #{inspect(key)}), error(key)}
{key, {:array, :integer}} ->
- {label(key), ~s(multiple_select form, #{inspect(key)}, ["1": 1, "2": 2]), error(key)}
+ {label(key), ~s(multiple_select f, #{inspect(key)}, ["1": 1, "2": 2]), error(key)}
{key, {:array, _}} ->
{label(key),
- ~s(multiple_select form, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"]),
+ ~s(multiple_select f, #{inspect(key)}, ["Option 1": "option1", "Option 2": "option2"]),
error(key)}
{key, _} ->
- {label(key), ~s(text_input form, #{inspect(key)}), error(key)}
+ {label(key), ~s(text_input f, #{inspect(key)}), error(key)}
end)
end
defp label(key) do
- ~s(phx_label form, #{inspect(key)})
+ ~s(label f, #{inspect(key)})
end
defp error(field) do
- ~s{partial error_tag(form, #{inspect(field)})}
+ ~s{error_tag(f, #{inspect(field)})}
end
end
end
diff --git a/lib/temple.ex b/lib/temple.ex
index 5899061..6a09900 100644
--- a/lib/temple.ex
+++ b/lib/temple.ex
@@ -1,182 +1,423 @@
defmodule Temple do
+ alias Temple.Buffer
+
+ @moduledoc """
+ > Warning: Docs are WIP
+
+ Temple syntax is available inside the `temple` and `live_temple` macros, and is compiled into EEx at build time.
+
+ ### Usage
+
+ ```elixir
+ temple do
+ # You can define attributes by passing a keyword list to the element, the values can be literals or variables.
+ class = "text-blue"
+ id = "jumbotron"
+
+ div class: class, id: id do
+ # Text nodes can be emitted as string literals or variables.
+ "Bob"
+
+ id
+ end
+
+ # if and unless expressions can be used to conditionally render content
+ if 5 > 0 do
+ p do
+ "Greater than 0!"
+ end
+ end
+
+ unless 5 > 0 do
+ p do
+ "Less than 0!"
+ end
+ end
+
+ # You can loop over items using for comprehensions
+ for x <- 0..5 do
+ div do
+ x
+ end
+ end
+
+ # You can use multiline anonymous functions, like if you're building a form in Phoenix
+ form_for @changeset, Routes.user_path(@conn, :create), fn f ->
+ "Name: "
+ text_input f, :name
+ end
+
+ # You can explicitly call a tag by prefixing with the Temple module
+ Temple.div do
+ "Foo"
+ end
+
+ # You can also pass children as a do key instead of a block
+ div do: "Alice", class: "text-yellow"
+ end
+ ```
+
+ ### Reserved keywords
+
+ You can pass a keyword list to an element as element attributes, but there are several reserved keywords.
+
+ #### Compact
+
+ Passing `compact: true` will not rendering new lines from within the element. This is useful if you are trying to use the `:empty` psuedo selector.
+
+ ```elixir
+ temple do
+ p compact: true do
+ "Foo"
+ end
+ p do
+ "Bar"
+ end
+ end
+ ```
+
+ would evaluate to
+
+ ```html
+
Foo
+
+ Bar
+
+ ```
+
+ ### Configuration
+
+ #### Aliases
+
+ You can add an alias for an element if there is a namespace collision with a function. If you are using `Phoenix.HTML`, there will be namespace collisions with the `` and `