├── .gitignore ├── README.md ├── assets ├── .gitignore ├── README.md ├── config │ ├── env.js │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── paths.js │ ├── polyfills.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── webpackDevServer.config.js ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── scripts │ ├── build.js │ ├── start.js │ └── test.js ├── src │ ├── App.test.js │ ├── actions │ │ ├── channel.js │ │ └── index.js │ ├── components │ │ ├── AddTodo.jsx │ │ ├── Footer.jsx │ │ ├── Todo.jsx │ │ └── TodoList.jsx │ ├── containers │ │ └── App.jsx │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reducers │ │ └── index.js │ ├── registerServiceWorker.js │ └── vendor │ │ └── phoenix.js └── yarn.lock ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── todo_test.ex ├── todo_test │ ├── application.ex │ ├── repo.ex │ └── todos │ │ ├── todo.ex │ │ └── todos.ex ├── todo_test_web.ex └── todo_test_web │ ├── channels │ ├── todo_channel.ex │ └── user_socket.ex │ ├── controllers │ ├── fallback_controller.ex │ ├── page_controller.ex │ └── todo_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── router.ex │ ├── templates │ ├── layout │ │ ├── app.html.eex │ │ └── app.html.eex.back │ └── page │ │ └── index.html.eex │ └── views │ ├── changeset_view.ex │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex │ ├── page_view.ex │ └── todo_view.ex ├── mix.exs ├── mix.lock ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot ├── repo │ ├── migrations │ │ ├── 20171026014808_create_todo.exs │ │ └── 20171026170000_add_todo_completed_field.exs │ └── seeds.exs └── static │ ├── asset-manifest.json │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ ├── robots.txt │ ├── service-worker.js │ └── static │ ├── css │ ├── main.65027555.css │ └── main.65027555.css.map │ └── js │ ├── main.065d4dbc.js │ └── main.065d4dbc.js.map ├── rel └── config.exs └── test ├── support ├── channel_case.ex ├── conn_case.ex └── data_case.ex ├── test_helper.exs ├── todo_test └── todos │ └── todos_test.exs └── todo_test_web ├── channels └── todo_channel_test.exs ├── controllers ├── page_controller_test.exs └── todo_controller_test.exs └── views ├── error_view_test.exs ├── layout_view_test.exs └── page_view_test.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Files matching config/*.secret.exs pattern contain sensitive 11 | # data and you should not commit them into version control. 12 | # 13 | # Alternatively, you may comment the line below and commit the 14 | # secrets files as long as you replace their contents by environment 15 | # variables. 16 | /config/*.secret.exs 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TodoTest 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | * Start Phoenix endpoint with `mix phx.server` 8 | 9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 10 | 11 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 12 | 13 | ## Learn more 14 | 15 | * Official website: http://www.phoenixframework.org/ 16 | * Guides: http://phoenixframework.org/docs/overview 17 | * Docs: https://hexdocs.pm/phoenix 18 | * Mailing list: http://groups.google.com/group/phoenix-talk 19 | * Source: https://github.com/phoenixframework/phoenix 20 | -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /assets/config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce((env, key) => { 82 | env[key] = JSON.stringify(raw[key]); 83 | return env; 84 | }, {}), 85 | }; 86 | 87 | return { raw, stringified }; 88 | } 89 | 90 | module.exports = getClientEnvironment; 91 | -------------------------------------------------------------------------------- /assets/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /assets/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /assets/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/todo_test_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= gettext "Welcome to %{name}!", name: "Phoenix" %>

3 |

A productive web framework that
does not compromise speed and maintainability.

4 |
5 | 6 |
7 |
8 |

Resources

9 | 20 |
21 | 22 |
23 |

Help

24 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /lib/todo_test_web/views/changeset_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.ChangesetView do 2 | use TodoTestWeb, :view 3 | 4 | @doc """ 5 | Traverses and translates changeset errors. 6 | 7 | See `Ecto.Changeset.traverse_errors/2` and 8 | `TodoTestWeb.ErrorHelpers.translate_error/1` for more details. 9 | """ 10 | def translate_errors(changeset) do 11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1) 12 | end 13 | 14 | def render("error.json", %{changeset: changeset}) do 15 | # When encoded, the changeset returns its errors 16 | # as a JSON object. So we just pass it forward. 17 | %{errors: translate_errors(changeset)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/todo_test_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) -> 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(TodoTestWeb.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(TodoTestWeb.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/todo_test_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.ErrorView do 2 | use TodoTestWeb, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/todo_test_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.LayoutView do 2 | use TodoTestWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/todo_test_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.PageView do 2 | use TodoTestWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/todo_test_web/views/todo_view.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.TodoView do 2 | use TodoTestWeb, :view 3 | alias TodoTestWeb.TodoView 4 | 5 | def render("index.json", %{todo: todo}) do 6 | %{data: render_many(todo, TodoView, "todo.json")} 7 | end 8 | 9 | def render("show.json", %{todo: todo}) do 10 | %{data: render_one(todo, TodoView, "todo.json")} 11 | end 12 | 13 | def render("todo.json", %{todo: todo}) do 14 | %{id: todo.id, 15 | label: todo.label, 16 | completed: todo.completed} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTest.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo_test, 7 | version: "0.0.1", 8 | elixir: "~> 1.4", 9 | elixirc_paths: elixirc_paths(Mix.env), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {TodoTest.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.3.0"}, 37 | {:phoenix_pubsub, "~> 1.0"}, 38 | {:phoenix_ecto, "~> 3.2"}, 39 | {:postgrex, ">= 0.0.0"}, 40 | {:phoenix_html, "~> 2.10"}, 41 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 42 | {:gettext, "~> 0.11"}, 43 | {:cowboy, "~> 1.0"}, 44 | {:distillery, "~> 1.5", runtime: false} 45 | ] 46 | end 47 | 48 | # Aliases are shortcuts or tasks specific to the current project. 49 | # For example, to create, migrate and run the seeds file at once: 50 | # 51 | # $ mix ecto.setup 52 | # 53 | # See the documentation for `Mix` for more info on aliases. 54 | defp aliases do 55 | [ 56 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 57 | "ecto.reset": ["ecto.drop", "ecto.setup"], 58 | "test": ["ecto.create --quiet", "ecto.migrate", "test"] 59 | ] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [], [], "hexpm"}, 2 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [], [], "hexpm"}, 4 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 5 | "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [], [], "hexpm"}, 6 | "distillery": {:hex, :distillery, "1.5.2", "eec18b2d37b55b0bcb670cf2bcf64228ed38ce8b046bb30a9b636a6f5a4c0080", [], [], "hexpm"}, 7 | "ecto": {:hex, :ecto, "2.2.6", "3fd1067661d6d64851a0d4db9acd9e884c00d2d1aa41cc09da687226cf894661", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [], [], "hexpm"}, 9 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"}, 10 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"}, 11 | "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [], [{:ecto, "~> 2.1", [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"}, 13 | "phoenix_html": {:hex, :phoenix_html, "2.10.4", "d4f99c32d5dc4918b531fdf163e1fd7cf20acdd7703f16f5d02d4db36de803b7", [], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.3", "1d178429fc8950b12457d09c6afec247bfe1fcb6f36209e18fbb0221bdfe4d41", [], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [], [], "hexpm"}, 16 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 17 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}, 18 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [], [], "hexpm"}, 19 | "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [], [], "hexpm"}} 21 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /priv/repo/migrations/20171026014808_create_todo.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTest.Repo.Migrations.CreateTodo do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:todo) do 6 | add :label, :string 7 | 8 | timestamps() 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20171026170000_add_todo_completed_field.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTest.Repo.Migrations.AddTodoCompletedField do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:todo) do 6 | add :completed, :boolean 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # TodoTest.Repo.insert!(%TodoTest.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /priv/static/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.65027555.css", 3 | "main.css.map": "static/css/main.65027555.css.map", 4 | "main.js": "static/js/main.065d4dbc.js", 5 | "main.js.map": "static/js/main.065d4dbc.js.map" 6 | } -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blaze33/elixir-phoenix-react-redux-todo-list/319a84ceafe7141f0ff895da797537293f3552c1/priv/static/favicon.ico -------------------------------------------------------------------------------- /priv/static/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /priv/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /priv/static/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}var precacheConfig=[["/index.html","d3d57cefbbfec018f2dfd5a704ec1fec"],["/static/css/main.65027555.css","41e5e45b9b5d9ecaa09b72c11eed3386"],["/static/js/main.065d4dbc.js","9c4fd23dce36959e27bed74e9684e8de"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL("/index.html",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}); -------------------------------------------------------------------------------- /priv/static/static/css/main.65027555.css: -------------------------------------------------------------------------------- 1 | body{margin:0;padding:0;font-family:sans-serif} 2 | /*# sourceMappingURL=main.65027555.css.map*/ -------------------------------------------------------------------------------- /priv/static/static/css/main.65027555.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css"],"names":[],"mappings":"AAAA,KACE,SACA,UACA,sBAAwB","file":"static/css/main.65027555.css","sourcesContent":["body {\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/index.css"],"sourceRoot":""} -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | # Import all plugins from `rel/plugins` 2 | # They can then be used by adding `plugin MyPlugin` to 3 | # either an environment, or release definition, where 4 | # `MyPlugin` is the name of the plugin module. 5 | Path.join(["rel", "plugins", "*.exs"]) 6 | |> Path.wildcard() 7 | |> Enum.map(&Code.eval_file(&1)) 8 | 9 | use Mix.Releases.Config, 10 | # This sets the default release built by `mix release` 11 | default_release: :default, 12 | # This sets the default environment used by `mix release` 13 | default_environment: Mix.env() 14 | 15 | # For a full list of config options for both releases 16 | # and environments, visit https://hexdocs.pm/distillery/configuration.html 17 | 18 | 19 | # You may define one or more environments in this file, 20 | # an environment's settings will override those of a release 21 | # when building in that environment, this combination of release 22 | # and environment configuration is called a profile 23 | 24 | environment :dev do 25 | # If you are running Phoenix, you should make sure that 26 | # server: true is set and the code reloader is disabled, 27 | # even in dev mode. 28 | # It is recommended that you build with MIX_ENV=prod and pass 29 | # the --env flag to Distillery explicitly if you want to use 30 | # dev mode. 31 | set dev_mode: true 32 | set include_erts: false 33 | set cookie: :"lgPzVTWD{3_JB)D7m_Nlu!e~{t(1^_~t]kGC,[kO?fknjI*jqDgYSDgJY$[^`Jt<" 34 | end 35 | 36 | environment :prod do 37 | set include_erts: true 38 | set include_src: false 39 | set cookie: :"A(v*A&As138=eu$>9DCDSPHTeKxXPD*[!(t6`&ndX[t^/x~P2a;}_?b0794~@I%0" 40 | end 41 | 42 | # You may define one or more releases in this file. 43 | # If you have not set a default release, or selected one 44 | # when running `mix release`, the first release in the file 45 | # will be used by default 46 | 47 | release :todo_test do 48 | set version: current_version(:todo_test) 49 | set applications: [ 50 | :runtime_tools 51 | ] 52 | end 53 | 54 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint TodoTestWeb.Endpoint 25 | end 26 | end 27 | 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TodoTest.Repo) 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, {:shared, self()}) 33 | end 34 | :ok 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import TodoTestWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint TodoTestWeb.Endpoint 26 | end 27 | end 28 | 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TodoTest.Repo) 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, {:shared, self()}) 34 | end 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoTest.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias TodoTest.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import TodoTest.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TodoTest.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transform changeset errors to a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, :manual) 4 | 5 | -------------------------------------------------------------------------------- /test/todo_test/todos/todos_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTest.TodosTest do 2 | use TodoTest.DataCase 3 | 4 | alias TodoTest.Todos 5 | 6 | describe "todo" do 7 | alias TodoTest.Todos.Todo 8 | 9 | @valid_attrs %{label: "some label"} 10 | @update_attrs %{label: "some updated label"} 11 | @invalid_attrs %{label: nil} 12 | 13 | def todo_fixture(attrs \\ %{}) do 14 | {:ok, todo} = 15 | attrs 16 | |> Enum.into(@valid_attrs) 17 | |> Todos.create_todo() 18 | 19 | todo 20 | end 21 | 22 | test "list_todo/0 returns all todo" do 23 | todo = todo_fixture() 24 | assert Todos.list_todo() == [todo] 25 | end 26 | 27 | test "get_todo!/1 returns the todo with given id" do 28 | todo = todo_fixture() 29 | assert Todos.get_todo!(todo.id) == todo 30 | end 31 | 32 | test "create_todo/1 with valid data creates a todo" do 33 | assert {:ok, %Todo{} = todo} = Todos.create_todo(@valid_attrs) 34 | assert todo.label == "some label" 35 | end 36 | 37 | test "create_todo/1 with invalid data returns error changeset" do 38 | assert {:error, %Ecto.Changeset{}} = Todos.create_todo(@invalid_attrs) 39 | end 40 | 41 | test "update_todo/2 with valid data updates the todo" do 42 | todo = todo_fixture() 43 | assert {:ok, todo} = Todos.update_todo(todo, @update_attrs) 44 | assert %Todo{} = todo 45 | assert todo.label == "some updated label" 46 | end 47 | 48 | test "update_todo/2 with invalid data returns error changeset" do 49 | todo = todo_fixture() 50 | assert {:error, %Ecto.Changeset{}} = Todos.update_todo(todo, @invalid_attrs) 51 | assert todo == Todos.get_todo!(todo.id) 52 | end 53 | 54 | test "delete_todo/1 deletes the todo" do 55 | todo = todo_fixture() 56 | assert {:ok, %Todo{}} = Todos.delete_todo(todo) 57 | assert_raise Ecto.NoResultsError, fn -> Todos.get_todo!(todo.id) end 58 | end 59 | 60 | test "change_todo/1 returns a todo changeset" do 61 | todo = todo_fixture() 62 | assert %Ecto.Changeset{} = Todos.change_todo(todo) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/todo_test_web/channels/todo_channel_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.TodoChannelTest do 2 | use TodoTestWeb.ChannelCase 3 | 4 | alias TodoTestWeb.TodoChannel 5 | 6 | setup do 7 | {:ok, _, socket} = 8 | socket("user_id", %{some: :assign}) 9 | |> subscribe_and_join(TodoChannel, "todo:lobby") 10 | 11 | {:ok, socket: socket} 12 | end 13 | 14 | test "ping replies with status ok", %{socket: socket} do 15 | ref = push socket, "ping", %{"hello" => "there"} 16 | assert_reply ref, :ok, %{"hello" => "there"} 17 | end 18 | 19 | test "shout broadcasts to todo:lobby", %{socket: socket} do 20 | push socket, "shout", %{"hello" => "all"} 21 | assert_broadcast "shout", %{"hello" => "all"} 22 | end 23 | 24 | test "broadcasts are pushed to the client", %{socket: socket} do 25 | broadcast_from! socket, "broadcast", %{"some" => "data"} 26 | assert_push "broadcast", %{"some" => "data"} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/todo_test_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.PageControllerTest do 2 | use TodoTestWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/todo_test_web/controllers/todo_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.TodoControllerTest do 2 | use TodoTestWeb.ConnCase 3 | 4 | alias TodoTest.Todos 5 | alias TodoTest.Todos.Todo 6 | 7 | @create_attrs %{label: "some label"} 8 | @update_attrs %{label: "some updated label"} 9 | @invalid_attrs %{label: nil} 10 | 11 | def fixture(:todo) do 12 | {:ok, todo} = Todos.create_todo(@create_attrs) 13 | todo 14 | end 15 | 16 | setup %{conn: conn} do 17 | {:ok, conn: put_req_header(conn, "accept", "application/json")} 18 | end 19 | 20 | describe "index" do 21 | test "lists all todo", %{conn: conn} do 22 | conn = get conn, todo_path(conn, :index) 23 | assert json_response(conn, 200)["data"] == [] 24 | end 25 | end 26 | 27 | describe "create todo" do 28 | test "renders todo when data is valid", %{conn: conn} do 29 | conn = post conn, todo_path(conn, :create), todo: @create_attrs 30 | assert %{"id" => id} = json_response(conn, 201)["data"] 31 | 32 | conn = get conn, todo_path(conn, :show, id) 33 | assert json_response(conn, 200)["data"] == %{ 34 | "id" => id, 35 | "label" => "some label"} 36 | end 37 | 38 | test "renders errors when data is invalid", %{conn: conn} do 39 | conn = post conn, todo_path(conn, :create), todo: @invalid_attrs 40 | assert json_response(conn, 422)["errors"] != %{} 41 | end 42 | end 43 | 44 | describe "update todo" do 45 | setup [:create_todo] 46 | 47 | test "renders todo when data is valid", %{conn: conn, todo: %Todo{id: id} = todo} do 48 | conn = put conn, todo_path(conn, :update, todo), todo: @update_attrs 49 | assert %{"id" => ^id} = json_response(conn, 200)["data"] 50 | 51 | conn = get conn, todo_path(conn, :show, id) 52 | assert json_response(conn, 200)["data"] == %{ 53 | "id" => id, 54 | "label" => "some updated label"} 55 | end 56 | 57 | test "renders errors when data is invalid", %{conn: conn, todo: todo} do 58 | conn = put conn, todo_path(conn, :update, todo), todo: @invalid_attrs 59 | assert json_response(conn, 422)["errors"] != %{} 60 | end 61 | end 62 | 63 | describe "delete todo" do 64 | setup [:create_todo] 65 | 66 | test "deletes chosen todo", %{conn: conn, todo: todo} do 67 | conn = delete conn, todo_path(conn, :delete, todo) 68 | assert response(conn, 204) 69 | assert_error_sent 404, fn -> 70 | get conn, todo_path(conn, :show, todo) 71 | end 72 | end 73 | end 74 | 75 | defp create_todo(_) do 76 | todo = fixture(:todo) 77 | {:ok, todo: todo} 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/todo_test_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.ErrorViewTest do 2 | use TodoTestWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(TodoTestWeb.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(TodoTestWeb.ErrorView, "500.html", []) == 14 | "Internal server error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(TodoTestWeb.ErrorView, "505.html", []) == 19 | "Internal server error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/todo_test_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.LayoutViewTest do 2 | use TodoTestWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/todo_test_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTestWeb.PageViewTest do 2 | use TodoTestWeb.ConnCase, async: true 3 | end 4 | --------------------------------------------------------------------------------