Skip to content

Commit

Permalink
Get system tests to be reliable
Browse files Browse the repository at this point in the history
  • Loading branch information
hopsoft committed Feb 25, 2024
1 parent 4344d4a commit 763fa5a
Show file tree
Hide file tree
Showing 34 changed files with 277 additions and 173 deletions.
6 changes: 6 additions & 0 deletions .byebug_history
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
continue
element("[data-test=allow]")
continue
element("[data-test=allow]")
continue
element("[data-test=allow]")
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
.yarn*
/.bundle/
/doc/
/log/*.log
/log/*.log*
/node_modules/
/pkg/
/test/dummy/db/*.sqlite3
Expand All @@ -18,3 +18,4 @@
/tmp/
Gemfile.lock
test/dummy/app/javascript/@turbo-boost
test/dummy/log/*.log*
2 changes: 1 addition & 1 deletion app/assets/builds/@turbo-boost/commands.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions app/assets/builds/@turbo-boost/commands.js.map

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions app/javascript/renderer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import uuids from './uuids'

// Morphs the element with the given HTML via the TurboBoost invoke <turbo-stream>
const morph = (selector, html) => {
const stream = document.createElement('turbo-stream')
stream.setAttribute('action', 'invoke')
stream.setAttribute('target', 'DOM')

const template = document.createElement('template')
template.content.textContent = JSON.stringify({
id: `morph-${uuids.v4()}`,
selector,
method: 'morph',
args: [html],
delay: 0
})

stream.appendChild(template)
document.body.appendChild(stream)
}

const append = content => {
document.body.insertAdjacentHTML('beforeend', content)
}

// TODO: Revisit the "Replace" strategy after morph ships with Turbo 8
const replace = content => {
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'text/html')
document.head.innerHTML = doc.head.innerHTML
document.body.innerHTML = doc.body.innerHTML
TurboBoost.Streams.morph(document.documentElement, doc.documentElement)
}

export const render = (strategy, content) => {
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ x-default-app: &default_app
- primary
volumes:
- .:/app
- ../turbo_boost-streams:/tbs
- external:/mnt/external
- node_modules:/app/node_modules

Expand Down
21 changes: 1 addition & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
"author": "Nate Hopkins (hopsoft) <[email protected]>",
"license": "MIT",
"peerDependencies": {
"@hotwired/turbo-rails": ">= 7.2.0",
"@turbo-boost/streams": ">= 0.1.10"
"@hotwired/turbo-rails": ">= 7.2.0"
},
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
Expand Down
69 changes: 57 additions & 12 deletions test/application_system_test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,71 @@ def js(...)
@page.evaluate(...)
end

# Waits for a specific element to appear on the page.
# Returns an element that matches the given testid attribute value.
#
# @param selector [String] A CSS selector
# @param testid [String,Symbol] The element's data-testid attribute value
# @return [Playwright::ElementHandle] The element
def element(selector)
parent = page.wait_for_selector(self.class::PARENT_SELECTOR) if defined?(self.class::PARENT_SELECTOR)
parent ||= page
parent.wait_for_selector selector
def element(testid)
page.get_by_test_id testid.to_s
end

# Waits for an element to be mutated (i.e. have its attributes or children changed).
# SEE: test/dummy/app/javascript/tests/index.js
#
# @param testid [String,Symbol] The element's data-testid attribute value
# @param timeout [Integer] The maximum time to wait (default: 2s)
# @param interval [Integer] The time interval to sleep between checks (default: 100ms)
# @param reset [Boolean] Whether to also wait for the mutation tracking to reset (default: true)
def wait_for_mutations(testid, timeout: 2.seconds, interval: 0.1, reset: true)
Timeout.timeout timeout.to_i do
while js("element => !element.mutations", arg: element(testid).element_handle)
sleep interval.to_f
end
end
wait_for_mutations_reset testid if reset
end

# Waits for an element's mutation tracking to reset.
# SEE: test/dummy/app/javascript/tests/index.js
#
# @param testid [String,Symbol] The element's data-testid attribute value
# @param timeout [Integer] The maximum time to wait (default: 2s)
# @param interval [Integer] The time interval to sleep between checks (default: 100ms)
def wait_for_mutations_reset(testid, timeout: 2.seconds, interval: 0.1)
Timeout.timeout timeout.to_i do
while js("element => element.mutations", arg: element(testid).element_handle)
sleep interval.to_f
end
end
end

# Waits for an element to be detached from the DOM.
#
# @param element [Playwright::ElementHandle] The element
# @param timeout [Integer] The maximum time to wait (default: 2s)
# @param interval [Integer] The time interval to sleep between checks (default: 50ms)
def wait_for_detach(element, timeout: 2.seconds, interval: 0.05)
start = Time.now
while page.evaluate("(element) => element.isConnected", arg: element)
break if Time.now - start > timeout
sleep interval
# @param interval [Integer] The time interval to sleep between checks (default: 100ms)
def wait_for_detach(element, timeout: 2.seconds, interval: 0.1)
Timeout.timeout timeout.to_i do
while page.evaluate("(element) => element.isConnected", arg: element)
sleep interval.to_f
end
end
end

def wait_for_turbo_boost(testid, timeout: 2.seconds, interval: 0.1, reset: true)
Timeout.timeout timeout.to_i do
while js("element => !element.hasAttribute('data-turbo-boost')", arg: element(testid).element_handle)
sleep interval.to_f
end
end
wait_for_turbo_boost_reset testid if reset
end

def wait_for_turbo_boost_reset(testid, timeout: 2.seconds, interval: 0.1)
Timeout.timeout timeout.to_i do
while js("element => element.hasAttribute('data-turbo-boost')", arg: element(testid).element_handle)
sleep interval.to_f
end
end
end
end
4 changes: 4 additions & 0 deletions test/dummy/app/assets/stylesheets/pico.css
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
@import 'https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css';

[aria-busy='true']::before {
display: none !important;
}
1 change: 1 addition & 0 deletions test/dummy/app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ import '@hotwired/turbo-rails'
import 'flowbite'
import './turbo-boost'
import './controllers'
import './tests'
51 changes: 51 additions & 0 deletions test/dummy/app/javascript/tests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const ttl = 150

// Node monkey patches for tests..............................................................................
function setMutated(type) {
clearTimeout(this._mutationsTimeout)
this._mutations = this._mutations || new Set()
this._mutations.add(type)
this._mutationsTimeout = setTimeout(() => delete this._mutations, ttl)
}

function mutations() {
let context = this
while (context && !context._mutations) context = context.parentNode
if (context?._mutations) return Array.from(context._mutations)
return null
}

Node.prototype.setMutated = setMutated
HTMLElement.prototype.setMutated = setMutated
Object.defineProperty(Node.prototype, 'mutations', { get: mutations })
Object.defineProperty(HTMLElement.prototype, 'mutations', { get: mutations })

// MutationObserver to track mutations for tests..............................................................
function callback(mutations, observer) {
mutations.forEach(mutation => {
if (mutation.type === 'attributes') mutation.target.setMutated('attributes')
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => node.setMutated('added'))
mutation.removedNodes.forEach(node => node.setMutated('removed'))
}
})
}

new MutationObserver(callback).observe(document.documentElement, {
attributes: true,
attributeOldValue: false,
childList: true,
subtree: true
})

// Resets TurboBoost tracking.................................................................................
let turboBoostTimeout

self.addEventListener('turbo-boost:command:finish', () => {
clearTimeout(turboBoostTimeout)
turboBoostTimeout = setTimeout(
() =>
document.querySelectorAll('[data-turbo-boost]').forEach(el => el.removeAttribute('data-turbo-boost')),
ttl
)
})
4 changes: 2 additions & 2 deletions test/dummy/app/views/layouts/pico.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
<h1>TurboBoost Commands</h1>
</header>

<main>
<section data-testid="all">
<%= yield %>
</main>
</section>

<footer>
<small>© 2024 Hopsoft. All rights reserved.</small>
Expand Down
3 changes: 1 addition & 2 deletions test/dummy/app/views/partials/_details.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<%= tag.details id: id,
open: turbo_boost.command_state.dig(id, :attributes, :open),
data: {
test: id,
controller: "state",
action: "toggle->state#saveAttributes",
state_attributes_value: ["open"].to_json
} do %>
}.merge(data) do %>
<%= yield %>
<% end %>
4 changes: 2 additions & 2 deletions test/dummy/app/views/tests/drivers/_footer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<small>
Message →
<% if @message %>
<ins data-test="message"><%= @message %></ins>
<%= tag.ins @message, data: data %>
<% else %>
<span data-test="message">...</span>
<%= tag.del "...", data: data %>
<% end %>
<br>
Request Format → <code><%= request.format %></code>
Expand Down
8 changes: 4 additions & 4 deletions test/dummy/app/views/tests/drivers/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= turbo_frame_tag "form-driver-test", data: { src: test_path(:form) } do %>
<%= render "partials/details", id: "form-driver" do %>
<%= render "partials/details", id: :form_driver, data: { testid: :form_driver, turbo_boost: request.format.turbo_boost? } do %>
<summary>
Form
<small><code>→ app/javascript/drivers/form.js</code></small>
Expand All @@ -8,7 +8,7 @@
<article>
<section>
<%= form_with url: tests_path, data: { turbo_command: "Drivers::Form::PreventControllerActionCommand" } do |form| %>
<button type="submit" data-test="prevent">
<button type="submit" data-testid="form_driver_prevent">
<article>Click to Invoke → <code>Drivers::Form::<ins><u>Prevent</u></ins>ControllerActionCommand</code></article>
<small>Uses the <u>Append</u> rendering strategy</small>
</button>
Expand All @@ -17,14 +17,14 @@

<section>
<%= form_with url: tests_path, data: { turbo_command: "Drivers::Form::AllowControllerActionCommand" } do |form| %>
<button type="submit" data-test="allow">
<button type="submit" data-testid="form_driver_allow">
<article>Click to Invoke → <code>Drivers::Form::<ins><u>Allow</u></ins>ControllerActionCommand</code></article>
<small>Uses the <u>Append</u> rendering strategy</small>
</button>
<% end %>
</section>

<%= render "/tests/drivers/footer" %>
<%= render "/tests/drivers/footer", data: { testid: :form_driver_message } %>
</article>
<% end %>
<% end %>
8 changes: 4 additions & 4 deletions test/dummy/app/views/tests/drivers/_frame.html.erb
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<%= turbo_frame_tag "frame-driver-test", data: { src: test_path(:frame) } do %>
<%= render "partials/details", id: "frame-driver" do %>
<%= render "partials/details", id: :frame_driver, data: { testid: :frame_driver, turbo_boost: request.format.turbo_boost? } do %>
<summary>
Frame
<small><code>→ app/javascript/drivers/frame.js</code></small>
</summary>

<article>
<section>
<button type="submit" data-test="prevent" data-turbo-command="Drivers::Frame::PreventControllerActionCommand">
<button type="submit" data-testid="frame_driver_prevent" data-turbo-command="Drivers::Frame::PreventControllerActionCommand">
<article>Click to Invoke → <code>Drivers::Frame::<ins><u>Prevent</u></ins>ControllerActionCommand</code></article>
<small>Uses the <u>Append</u> rendering strategy</small>
</button>
</section>

<section>
<button type="submit" data-test="allow" data-turbo-command="Drivers::Frame::AllowControllerActionCommand">
<button type="submit" data-testid="frame_driver_allow" data-turbo-command="Drivers::Frame::AllowControllerActionCommand">
<article>
Click to Invoke → <code>Drivers::Frame::<ins><u>Allow</u></ins>ControllerActionCommand</code>
<br>
Expand All @@ -24,7 +24,7 @@
</button>
</section>

<%= render "/tests/drivers/footer" %>
<%= render "/tests/drivers/footer", data: { testid: :frame_driver_message } %>
</article>
<% end %>
<% end %>
8 changes: 4 additions & 4 deletions test/dummy/app/views/tests/drivers/_method.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= turbo_frame_tag "method-driver-test", data: { src: test_path(:method) } do %>
<%= render "partials/details", id: "method-driver" do %>
<%= render "partials/details", id: :method_driver, data: { testid: :method_driver, turbo_boost: request.format.turbo_boost? } do %>
<summary>
Method
<small><code>→ app/javascript/drivers/method.js</code></small>
Expand All @@ -8,21 +8,21 @@
<article>
<section>
<%= link_to test_path(:method), role: "button", style: "width:100%",
data: { test: "prevent", turbo_method: "delete", turbo_command: "Drivers::Method::PreventControllerActionCommand" } do %>
data: { testid: :method_driver_prevent, turbo_method: "delete", turbo_command: "Drivers::Method::PreventControllerActionCommand" } do %>
<article>Click to Invoke ➜ <code>Drivers::Method::<ins><u>Prevent</u></ins>ControllerActionCommand</code></article>
<small>Uses the <u>Append</u> rendering strategy</small>
<% end %>
</section>

<section>
<%= link_to test_path(:method), role: "button", style: "width:100%",
data: { test: "allow", turbo_method: "delete", turbo_command: "Drivers::Method::AllowControllerActionCommand" } do %>
data: { testid: :method_driver_allow, turbo_method: "delete", turbo_command: "Drivers::Method::AllowControllerActionCommand" } do %>
<article>Click to Invoke ➜ <code>Drivers::Method::<ins><u>Allow</u></ins>ControllerActionCommand</code></article>
<small>Uses the <u>Append</u> rendering strategy</small>
<% end %>
</section>

<%= render "/tests/drivers/footer" %>
<%= render "/tests/drivers/footer", data: { testid: :method_driver_message } %>
</article>
<% end %>
<% end %>
Loading

0 comments on commit 763fa5a

Please sign in to comment.