Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# AGENTS.md

Cuprite is a Capybara driver for Chrome built on Ferrum. It adds Capybara's driver contract and the behavior that
needs Capybara or driver context on top of Ferrum; everything else belongs to Ferrum.

## Commands

```sh
bundle exec rake # whole suite, what CI runs
bundle exec rspec spec/node_spec.rb # one file
bundle exec rubocop # lint, must be clean
HEADLESS=false bundle exec rspec ... # watch the browser
```

## Tests

- **Cover only what Cuprite owns**: Capybara's driver contract (run by `spec/capybara_spec.rb`) and what Cuprite
overrides or adds on top of Ferrum. Don't re-test Ferrum's behavior reached through `page.driver.browser` or a thin
delegate; Ferrum's suite covers it.
- **Prefer feature specs**: drive a real browser against the test app and assert on observable behavior.
- **Unit specs go in `spec/unit/` and nowhere else**, mirroring the `lib/capybara/cuprite` path:
`lib/capybara/cuprite/driver.rb` -> `spec/unit/driver_spec.rb`.
- Don't test private methods; cover them through the public API.
- Structure every spec file in one of two shapes, nothing else:

```ruby
describe Capybara::Cuprite::Node do
describe "#click" do
it "hovers the element before clicking it" do
```

```ruby
describe Capybara::Cuprite::Node do
describe "#select_option" do
context "when the option is in an optgroup" do
it "fires the change event" do
```

The top `describe` is the class, and the file path mirrors that class's path in `lib/capybara/cuprite`, for
feature and unit specs alike: `Capybara::Cuprite::Browser::Extensions` -> `spec/browser/extensions_spec.rb`.
Each nested `describe` names one public method (`"#method"`, `".method"` for class methods), never a topic.
`spec/capybara_spec.rb`, which runs Capybara's shared suite, is the one exception.
- Use contexts only for contrasting groups, at least two that oppose each other (`"when the option is in an
optgroup"` / `"when the option is not in an optgroup"`). A single condition isn't a context: keep the examples
flat and put the condition in the example name, e.g. `it "does not emulate a mobile browser without emulate:"`.
The exception is a context that carries `let`, hooks (`before`/`after`/`around`) or tags its examples share; it
may sit next to plain examples. Never nest contexts.
- Every example must be runnable alone with `rspec spec/file_spec.rb:LINE`, so each one gets its own `it` on its
own line. No `shared_examples`/`include_examples` and no loops that generate examples. Share the steps and
assertions through a helper method in the `describe` instead, and call it from one-line examples, one per variant.
- Start every example name with a verb in the third person: "fires the change event", "returns the focused
element". Never "must ...", "should ...", "can ..." or a bare noun.
- Almost no comments in specs; the example name says what is checked.
- For a bug fix, make sure the new spec fails without the fix and passes with it.

## Code style

- `# frozen_string_literal: true`, double quotes, RuboCop config in `.rubocop.yml`.
- Prefer self-documenting code over comments. Document public methods with YARD.
- American English everywhere: code, comments, docs, errors, commits.

## Git

- Conventional commits: `<type>(<scope>): <subject>`, type one of `feat|fix|docs|style|ref|test|chore|perf`,
imperative mood, no trailing period, subject <= 100 chars. Add a body for non-trivial changes.
- One logical change per commit.
- No AI attribution of any kind: no `Co-Authored-By`, no session links.
- Work on a branch, commit locally. Never push or open a PR unless asked.
175 changes: 175 additions & 0 deletions spec/browser/extensions_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# frozen_string_literal: true

describe Capybara::Cuprite::Browser::Extensions do
let(:mobile) { false }

around do |example|
@session = mobile ? TestSessions::CupriteMobile : TestSessions::Cuprite
@driver = @session.driver
example.run
ensure
@driver.reset!
end

describe "#url_blacklist=" do
it "blocks unwanted urls" do
@driver.browser.url_blacklist = /unwanted/

@session.visit "/cuprite/url_blacklist"

expect(@session.status_code).to eq(200)
expect(@session).to have_content("We are loading some unwanted action here")
@session.within_frame "framename" do
expect(@session.html).not_to include("We shouldn't see this.")
end
end

it "blocks urls matching a regexp" do
@driver.browser.url_blacklist = /.*wanted/

@session.visit "/cuprite/url_whitelist"

expect(@session.status_code).to eq(200)
expect(@session).to have_content("We are loading some wanted action here")
@session.within_frame "framename" do
expect(@session).not_to have_content("We should see this.")
end
@session.within_frame "unwantedframe" do
expect(@session).not_to have_content("We shouldn't see this.")
end
end

it "keeps blocking urls from the driver option after reset!" do
Capybara.register_driver :cuprite_blacklist do |app|
Capybara::Cuprite::Driver.new(app, @driver.options.merge(url_blacklist: /unwanted/))
end

session = Capybara::Session.new(:cuprite_blacklist, @session.app)

session.visit "/cuprite/url_blacklist"
expect(session).to have_content("We are loading some unwanted action here")
session.within_frame "framename" do
expect(session.html).not_to include("We shouldn't see this.")
end

session.reset!

session.visit "/cuprite/url_blacklist"
expect(session).to have_content("We are loading some unwanted action here")
session.within_frame "framename" do
expect(session.html).not_to include("We shouldn't see this.")
end
end
end

describe "#url_whitelist=" do
it "allows whitelisted urls" do
@driver.browser.url_whitelist = [/url_whitelist/, %r{/wanted}]

@session.visit "/cuprite/url_whitelist"

expect(@session.status_code).to eq(200)
expect(@session).to have_content("We are loading some wanted action here")
@session.within_frame "framename" do
expect(@session).to have_content("We should see this.")
end
@session.within_frame "unwantedframe" do
expect(@session).not_to have_content("We shouldn't see this.")
end
end

it "allows urls matching a regexp" do
@driver.browser.url_whitelist = [/url_whitelist/, %r{/.*wanted}]

@session.visit "/cuprite/url_whitelist"

expect(@session.status_code).to eq(200)
expect(@session).to have_content("We are loading some wanted action here")
@session.within_frame "framename" do
expect(@session).to have_content("We should see this.")
end
@session.within_frame "unwantedframe" do
expect(@session).to have_content("We shouldn't see this.")
end
end

it "raises when a blacklist is also set" do
@driver.browser.url_whitelist = /url_whitelist/
expect { @driver.browser.url_blacklist = /url_whitelist/ }
.to raise_error(ArgumentError, "You can't use blacklist along with whitelist")
end

it "allows urls when the whitelist is empty" do
@driver.browser.url_whitelist = []

@session.visit "/cuprite/url_whitelist"

expect(@session.status_code).to eq(200)
expect(@session).to have_content("We are loading some wanted action here")
@session.within_frame "framename" do
expect(@session).to have_content("We should see this.")
end
end

it "keeps allowing urls from the driver option after reset!" do
Capybara.register_driver :cuprite_whitelist do |app|
Capybara::Cuprite::Driver.new(app,
@driver.options.merge(url_whitelist: [/url_whitelist/, %r{/cuprite/wanted}]))
end

session = Capybara::Session.new(:cuprite_whitelist, @session.app)

session.visit "/cuprite/url_whitelist"
expect(session).to have_content("We are loading some wanted action here")
session.within_frame "framename" do
expect(session).to have_content("We should see this.")
end

session.within_frame "unwantedframe" do
# make sure non whitelisted urls are blocked
expect(session).not_to have_content("We shouldn't see this.")
end

session.reset!

session.visit "/cuprite/url_whitelist"
expect(session).to have_content("We are loading some wanted action here")
session.within_frame "framename" do
expect(session).to have_content("We should see this.")
end
session.within_frame "unwantedframe" do
# make sure non whitelisted urls are blocked
expect(session).not_to have_content("We shouldn't see this.")
end
end
end

describe "#raise_on_unhandled_modal=" do
it "accepts an unhandled modal with a warning by default" do
@session.visit "/cuprite/with_js"
expect(@session.driver.browser.page)
.to receive(:warn).with(
"Modal window with text `{T}ext \\w|th [reg.exp] (chara©+er$)?` has been opened, " \
"but you didn't wrap your code into (`accept_prompt` | `dismiss_prompt` | `accept_confirm` " \
"| `dismiss_confirm` | `accept_alert`), accepting by default"
)

expect { @session.click_link("Open for match") }.not_to raise_error

expect(@session).to have_xpath("//a[@id='open-match' and @confirmed='true']")
end

it "raises on an unhandled modal when true" do
@session.driver.browser.raise_on_unhandled_modal = true

@session.visit "/cuprite/with_js"

expect { @session.click_link("Open for match") }.to raise_error(
Capybara::Cuprite::UnhandledModalError,
"Modal window with text `{T}ext \\w|th [reg.exp] (chara©+er$)?` has been opened, " \
"but you didn't wrap your code into (`accept_prompt` | `dismiss_prompt` | `accept_confirm` " \
"| `dismiss_confirm` | `accept_alert`), accepting by default"
)
end
end
end
39 changes: 39 additions & 0 deletions spec/browser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

describe Capybara::Cuprite::Browser do
let(:mobile) { false }

around do |example|
@session = mobile ? TestSessions::CupriteMobile : TestSessions::Cuprite
@driver = @session.driver
example.run
ensure
@driver.reset!
end

def session_url(path)
server = @session.server
"http://#{server.host}:#{server.port}#{path}"
end

describe "#command" do
it "restarts the browser when it dies during a command" do
driver = Capybara::Cuprite::Driver.new(nil)
expect { driver.browser.crash }.to raise_error(Ferrum::DeadBrowserError)
driver.visit(session_url("/"))
expect(driver.html).to include("Hello world")
end
end

describe "#restart" do
it "starts over with empty network traffic" do
@session.visit("/cuprite/with_js")
expect(@driver.browser.network.traffic.length).to eq(4)

@driver.browser.restart

@session.visit("/cuprite/with_js")
expect(@driver.browser.network.traffic.length).to eq(4)
end
end
end
File renamed without changes.
Loading
Loading