Skip to content

Commit

Permalink
Add cheatsheets (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
Serabe authored Oct 15, 2023
1 parent 46c008f commit a04cbab
Show file tree
Hide file tree
Showing 6 changed files with 510 additions and 7 deletions.
146 changes: 146 additions & 0 deletions guides/cheatsheets/accessors.cheatmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Accessors Cheatsheet

Accessors let you access certains parts of the element you have selected previously.
As the given htmlable can contain multiple nodes, these functions return lists.

All function that can be filtered using a selector, they are done by using a selector
as second argument. In that case, there is a version of the same function ended with
`_in` where the first and second arguments are switched. These are just convenience
function in case you want to pipe when building a complex selector.

## Selecting nodes
{: .col-2}

#### All matching nodes

```elixir
find(view, "p")
find_in("p", view)
# Returns all paragraphs.
```

#### First matching node

```elixir
find_first(view, "div.contents")
find_first_in("div.contents", view)
# Returns the first div with contents class
```

#### Count the matching nodes

Just a convinience function.

```elixir
find_count(view, "p")
find_count_in("p", view)
# Counts the number of paragraphs

find_count(view, "nav ul li")
find_count_in("nav ul li", view)
# Counts the number of elements in the navigation list
```

## Selecting attributes
{: .col-2}

### Classes

Classes are quite common in HTML. Specially useful when checking for classes like
`hidden` in Tailwind, that checks if an element should be hidden or not. Class
attributes will be split in words before being returned.

#### One element

Look out for the nested list!

```elixir
classes(~s[<div class="one two three">Content</div>])
# [["one", "two", "three"]]
```

#### Several elements

```elixir
classes(~s[<div class="one two three">Content</div><div class="four five">Other</div>])
# [["one", "two", "three"], ["four", "five"]]
```

#### Using selectors
`classes/2` let's you pass a selector as second argument. This is convenient so you don't
to pipe `find` just for this.

```elixir
classes(~s[<div class="one two three">Content</div><div class="first">Other</div>], ".one")
classes_in(".one", ~s[<div class="one two three">Content</div><div class="first">Other</div>])
# [["one", "two", "three"]]
classes(~s[<div class="one two three">Content</div><div class="four five">Other</div>], "div")
classes_in("div", ~s[<div class="one two three">Content</div><div class="four five">Other</div>])
# [["one", "two", "three"], ["four", "five"]]
```

### Other attributes

Other attributes can easily be accessed too. Unlike classes, these values are returned
as strings, unparsed in any way.

#### One element

```elixir
attribute(~s[<li data-index="0">1</li>], "data-index")
# ["0"]
```

#### Several elements

```elixir
attribute(~s[<li data-index="0">1</li><li data-index="1">2</li>], "data-index")
# ["0", "1"]
```

#### Using selectors

`attribute/3` let's you pass a selector as second argument. This is convenient so you don't
to pipe `find` just for this.


```elixir
attribute(~s[<div class="one" data-index="0">Content</div><div class="first" data-index="1">Other</div>], ".one", "data-index")
attribute_in(".one", ~s[<div class="one" data-index="0">Content</div><div class="first" data-index="1">Other</div>], "data-index")
# ["0"]
attribute_in("div", ~s[<div class="one two three">Content</div><div class="four five">Other</div>], "class")
# ["one two three", "four five"]
```

### Text

#### Several elements, combined

Text would return just one string for all elements by default.

```elixir
text(~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>), ".odd")
text_in(".odd", ~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>))
# "First Third"

text(~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>), ".even")
text_in(".even", ~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>))
# "Second"

text(~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>), ".none")
text_in(".none", ~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>))
# ""

text(~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>), "li")
text_in("li", ~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>))
# "First Second Third"
```

#### Several elements, list

Combine `Enum.map/2` with `Nobs.Accessors.text/3` to create a list.

```elixir
find(~s(<ul><li class="odd"> First </li> <li class="even"> Second </li> <li class="odd"> Third </li></ul>), "li") |> Enum.map(&text/1)
# ~w(First Second Third)
```
71 changes: 71 additions & 0 deletions guides/cheatsheets/assertions.cheatmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Assertions Cheatsheet

These ways of asserting in tests give you some advantages over the "standard" way:

- Better errors. Most of these recipes rely on ExDoc to provide better errors when a
test fails (except for [`is_in?/2`](`DomHelpers.Assertions.is_in?/2`) which is just a
convenience function).
- Forces you to be more specific about what you are testing. The usual way of using
`render(view) =~ expected_content` is prone to false green and, when failing, makes
it harder to know the intent of the test.

## Elements existence
{: .col-2}

#### [`is_in?/2`](`DomHelpers.Assertions.is_in?/2`)

Just a convenience function that reverses the arguments of `Phoenix.LiveViewTest.has_element?/2`.

```elixir
assert "input"
|> with_attrs(type: "checkbox", name: {:ends_with, "[type]"})
|> is_in?(view)
```

#### Check the number of elements matching a selector

Other comparisons are possible, like greater than or less than.

```elixir
assert find_count(view, "[data-test=accordion]") == 2
```

## Elements attributes
{: .col-2}

#### Checking it has a class

```elixir
assert selector
|> with_class(expected_class)
|> is_in?(view)
assert expected_class in (find_first(view, selector) |> classes() |> List.first())
```

#### Checking it has certain attribute

```elixir
assert selector
|> with_attr("aria-expanded", "true")
|> is_in?(view)
assert [["true"]] ==
attribute(view, selector, "aria-expanded")
```

#### Checking the text of an element

When fails, you get a nice diff between both text. Also, `=~` is prone to false greens.

```elixir
assert text(view, "[data-test=my-element]") == "My content"
```

#### Multiple checks at once

No better errors here, except for text.

```elixir
selector = "button" |> with_class(".destroy") |> with_attrs(disabled: true, type: "submit")

assert text(view, selector) == "Submit form"
```
146 changes: 146 additions & 0 deletions guides/cheatsheets/selectors.cheatmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Selectors Cheatsheet

## Building selectors
{: .col-2}

### Adding class / classes to a selector

#### Just one class

```elixir
with_class("span", "hidden")
```

#### Several classes

```elixir
with_classes("span", ~w(contents grid))
```

### Checking no class in a selector

#### Just one class

```elixir
without_class("span", "hidden")
```

#### Several classes

```elixir
without_classes("span", ~w(contents grid))
```

## Id
{: .col-1}

### With id

```elixir
with_id("input", "form_field")
```

## Attributes
{: .col-2}

### Adding attributes to a selector

#### One attribute

```elixir
with_attr("button", "disabled")
with_attr("input", "type", "checkbox")
```

#### Several attributes

```elixir
with_attrs("input", type: "checkbox", name: "form[field]")
```

#### Checking an element does not have a selector.

```elixir
without_attr("button", "disabled")
without_attr("button", "phx-click", "remove-item")
```

### Multiple selector options

#### Checking just the existence of some attributes

If the value given to one attribute is just `true`, it'll just check for the existence without checking for the value.

```elixir
with_attrs("button", type: "button", disabled: true)
```

#### Checking that an attribute is not there

We can also check that an attribute is not there with `with_attrs`, just give `false` as value.

```elixir
with_attrs("button", type: "button", disabled: false)
```

### Use different matchers in attributes.

`dom_helpers` support _"modifiers"_ in the form of `{modifier, value}` that
can be used to change the matcher used in the selectors. [Documentation for these
selectors can be found in MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#syntax)

#### Exact match

Without a _"modifier"_, it just check for the exact value. There is also the `:equal`
modifier.

```elixir
with_attr("input", "type", "checkbox")
with_attr("input", "type", {:equal, "checkbox"})
with_attrs("input", type: "checkbox")
with_attrs("input", type: {:equal, "checkbox"})
```

#### Contains

Checks if the value given to the selector is contained within the actual value
in the element.

```
with_attr("input", "name", {:contains, "[certain_sub_field]"})
with_attrs("input", name: {:contains, "[certain_sub_field]"})
```

#### Contains word

With the `:contains_word` modifier, the `~=` matcher is used. This matcher
considers the value a list of whitespace-separated words and just checks that
one of the words is the given value to the selector.

```elixir
with_attr("span", "class", {:contains_word, "hidden"})
with_attrs("span", class: {:contains_word, "hidden"})

# Yeah, I know it would be best to use `with_class` for those examples.
```

#### Starts with / Ends with modifiers.

There are also a `:starts_with` modifier for the `^=` matcher and `:ends_with` modifier for
the `$=` matcher.

```elixir
with_attr("input", "name", {:starts_with, "form[field]"})
with_attrs("input", name: {:starts_with, "form[field]"})
with_attr("input", "name", {:ends_with, "[subfield][sub_subfield]"})
with_attrs("input", name: {:ends_with, "[subfield][sub_subfield]"})
```

#### Subcode

See MDN documentation for this one, as it is a bit tricky.

```elixir
with_attr("html", "lang", {:subcode, "es"})
with_attrs("html", lang: {:subcode, "es"})
```
Loading

0 comments on commit a04cbab

Please sign in to comment.