diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f040c3..33f2881a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## [Unreleased](https://github.com/rubycdp/ferrum/compare/v0.20.1...main) ## ### Added +- `cookies[name, domain:, path:]` picks one of the cookies sharing a name +- `context.cookies` with every cookie in the context whatever its domain, `browser.cookies` is the default context's - `network.emulate(user_agent:, accept_language:, platform:, metadata:)` overrides what the page reports in the request headers and in `navigator`, `platform` had no way to be set before, and `metadata` keeps the client hints Chrome otherwise stops sending once the user agent is overridden @@ -8,6 +10,9 @@ pending, when the page doesn't load in time ### Changed +- `page.cookies` is the cookies the page sees, the ones sent to its URL and its frames' URLs, or `each(urls:)`. It + returned every cookie in the context, use `browser.cookies` or `browser.contexts[page.context_id].cookies` for that. + `set` and `remove` default to the page's host, `clear` keeps other domains, `store`/`load` are on the context only - `NoSuchPageError` is a `NoSuchTargetError` now, so rescuing `NoSuchTargetError` covers a target Chrome no longer knows, like a closed page, as well as one Ferrum doesn't track - Every script runs the new way: keyword arguments are named parameters, promises and thenables are awaited, all @@ -15,6 +20,8 @@ [upgrading](docs/13-javascript.md#upgrading-to-021) [#673] ### Deprecated +- `cookies.all`, will be removed in 0.22. Keyed by name, it kept one of the cookies sharing a name, use `to_a`, or + `map(&:name)` for the names - `current_url` and `current_title` on pages and frames, will be removed in 0.22. Use `url` and `title`, or `frame.page.url` for the top window from a frame, `frame.url` is the frame's own - `:js_errors`, will be removed in 0.22. Uncaught exceptions are in `page.console` with `source: :exception`, use it or diff --git a/docs/11-cookies.md b/docs/11-cookies.md index 20fc1be0..b95f9b38 100644 --- a/docs/11-cookies.md +++ b/docs/11-cookies.md @@ -4,84 +4,157 @@ sidebar_position: 11 # Cookies -`page.cookies` +Cookies belong to a [context](https://github.com/rubycdp/ferrum#contexts), and every page in it shares them. +`context.cookies` has all of them, whatever their domain. `browser.cookies` is the cookies of the default context. +`page.cookies` narrows them down to what the page sees: the cookies sent to its URL and the URLs of its frames. -#### all : `Hash` +```ruby +# After logging in on example.com through sso.example.com +browser.cookies.map(&:name) # => ["session", "sso_token"] +browser.page.cookies.map(&:name) # => ["session"] +``` + +## Context cookies + +`browser.cookies`, `context.cookies` -Returns cookies hash +#### each : `Enumerator` + +All cookies in the context. Cookies are `Enumerable`, so `to_a`, `select`, `map` and the rest work too. ```ruby -page.cookies.all # => {"NID"=>#"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false}>} +browser.cookies.to_a # => [#"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false}>] +browser.cookies.select { |cookie| cookie.domain == ".google.com" } ``` -#### \[\](value) : `Cookie` +#### all : `Hash` + +Deprecated, will be removed in 0.22. Cookies keyed by name, so of the cookies sharing a name only one is kept. Use +`to_a`, or `map(&:name)` for the names. -Returns cookie +#### \[\](name, domain: `String`, path: `String`) : `Cookie` | `nil` -* value `String` +The first cookie with the given name. A name can be used by several cookies, on different domains or paths, `domain` +and `path` pick one of them. Both are exact, a cookie sent to every subdomain has a domain with a leading dot. + +* name `String` +* domain `String` +* path `String` ```ruby -page.cookies["NID"] # => "NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false}> +browser.cookies["NID"] # => "NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false}> +browser.cookies["id", domain: "www.example.com", path: "/app"] ``` -#### set(value) : `Boolean` +#### set(value) : `true` -Sets a cookie +Sets a cookie, for the `:base_url` host unless `:domain` or `:url` is given. * value `Hash` * :name `String` * :value `String` * :domain `String` + * :url `String` + * :path `String` * :expires `Integer` * :samesite `String` * :httponly `Boolean` + * :secure `Boolean` ```ruby -page.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true +browser.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true ``` * value `Cookie` ```ruby -nid_cookie = page.cookies["NID"] # => -page.cookies.set(nid_cookie) # => true +nid_cookie = browser.cookies["NID"] # => +browser.cookies.set(nid_cookie) # => true ``` -#### remove(\*\*options) : `Boolean` +#### remove(\*\*options) : `true` -Removes given cookie +Removes the cookies with the given name, on the exact domain, the `:base_url` host by default, or the ones sent to the +URL. * options `Hash` * :name `String` * :domain `String` * :url `String` + * :path `String` ```ruby -page.cookies.remove(name: "stealth", domain: "google.com") # => true +browser.cookies.remove(name: "stealth", domain: "google.com") # => true +browser.cookies.remove(name: "sso_token", url: "https://sso.example.com") # => true ``` -#### clear : `Boolean` +#### clear : `true` -Removes all cookies for current page +Removes all cookies in the context. ```ruby -page.cookies.clear # => true +browser.cookies.clear # => true ``` -#### store(path) : `Boolean` +#### store(path) : `Integer` -Stores all cookies of current page in a file. +Stores all cookies in the context in a file, `cookies.yml` by default. ```ruby -# Cookies are saved into cookies.yml -page.cookies.store # => 15657 +browser.cookies.store # => 15657 ``` -#### load(path) : `Boolean` +#### load(path) : `true` -Loads all cookies from the file and sets them for current page. +Loads the cookies from a file, `cookies.yml` by default, and sets them in the context. ```ruby -# Cookies are loaded from cookies.yml -page.cookies.load # => true +browser.cookies.load # => true +``` + +## Page cookies + +`page.cookies` + +#### each(urls: `Array`) : `Enumerator` + +The cookies sent to the page's URL and the URLs of its frames, or to the given URLs. Nothing before the page has +loaded a URL. `all` is deprecated here too. + +```ruby +page.cookies.map(&:name) # => ["session"] +page.cookies.each(urls: ["https://sso.example.com"]).map(&:name) # => ["sso_token"] +``` + +#### \[\](name, domain: `String`, path: `String`) : `Cookie` | `nil` + +The page's first cookie with the given name, narrowed down like the context's. + +```ruby +page.cookies["NID"] # => +``` + +#### set(value) : `Boolean` + +Sets a cookie, takes the same options as the context's `set`. Without `:domain` or `:url` it's set for the page's host, +or the `:base_url` host before the page has loaded anything. + +```ruby +page.cookies.set(name: "stealth", value: "omg") # => true +``` + +#### remove(\*\*options) : `true` + +Removes the cookies with the given name, on the page's host unless `:domain` or `:url` is given. + +```ruby +page.cookies.remove(name: "stealth") # => true +``` + +#### clear : `true` + +Removes the page's cookies, the ones `each` yields. Cookies on other domains stay. + +```ruby +page.cookies.clear # => true ``` diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index 0526fd06..49eb5764 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -27,12 +27,13 @@ class Browser extend Forwardable delegate %i[default_context] => :contexts - delegate %i[targets create_target page pages windows workers service_workers attach_target] => :default_context + delegate %i[targets create_target page pages windows workers service_workers attach_target + cookies] => :default_context delegate %i[go_to goto go back forward refresh reload stop wait_for_reload wait_for_navigation wait_for_navigation! at_css at_xpath css xpath wait_for_selector current_url current_title url title body doctype content= - headers cookies network console accessibility downloads + headers network console accessibility downloads mouse keyboard screenshot pdf mhtml viewport_size device_pixel_ratio start_screencast stop_screencast diff --git a/lib/ferrum/context.rb b/lib/ferrum/context.rb index 12b14310..2e833ed0 100644 --- a/lib/ferrum/context.rb +++ b/lib/ferrum/context.rb @@ -42,6 +42,13 @@ def default_target @default_target ||= create_target end + # The cookies of the context, which every page in it shares. + # + # @return [Cookies] + def cookies + @cookies ||= Cookies.new(@client, @id) + end + # Connects to and returns the {#default_target}'s page. # # @return [Page] diff --git a/lib/ferrum/cookies.rb b/lib/ferrum/cookies.rb index 07e38641..f16ed798 100644 --- a/lib/ferrum/cookies.rb +++ b/lib/ferrum/cookies.rb @@ -2,28 +2,32 @@ require "yaml" require "ferrum/cookies/cookie" +require "ferrum/cookies/collection" module Ferrum # - # Manages a page's cookies via the CDP `Network` domain. Enumerable over - # the page's current {Cookies::Cookie}s, and provides methods to read, - # set, remove and clear them, as well as persist them to/from a YAML file. + # Manages the cookies of a browser {Context}, which every page in it shares, + # via the CDP `Storage` domain. Enumerable over all of them whatever their + # domain, and provides methods to read, set, remove and clear them, as well + # as persist them to/from a YAML file. {Page::Cookies} narrows them down to + # what one page sees. # class Cookies - include Enumerable + include Collection - def initialize(page) - @page = page + def initialize(client, context_id) + @client = client + @context_id = context_id end # - # Enumerates over all cookies. + # Enumerates over all cookies in the context. # # @yield [cookie] # The given block will be passed each cookie. # # @yieldparam [Cookie] cookie - # A cookie in the browser. + # A cookie in the context. # # @return [Enumerator] # If no block is given, an Enumerator object will be returned. @@ -31,53 +35,13 @@ def initialize(page) def each return enum_for(__method__) unless block_given? - cookies = @page.command("Network.getAllCookies")["cookies"] + cookies = @client.command("Storage.getCookies", browserContextId: @context_id)["cookies"] cookies.each do |c| yield Cookie.new(c) end end - # - # Returns cookies hash. - # - # @return [Hash{String => Cookie}] - # - # @example - # browser.cookies.all # => { - # # "NID" => #"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", - # # "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false - # # }> - # # } - # - def all - each.to_h do |cookie| - [cookie.name, cookie] - end - end - - # - # Returns cookie. - # - # @param [String] name - # The cookie name to fetch. - # - # @return [Cookie, nil] - # The cookie with the matching name. - # - # @example - # browser.cookies["NID"] # => - # # "NID", "value"=>"...", "domain"=>".google.com", - # # "path"=>"/", "expires"=>1583211046.575681, "size"=>178, - # # "httpOnly"=>true, "secure"=>false, "session"=>false - # # }> - # - def [](name) - find { |cookie| cookie.name == name } - end - # # Sets a cookie. # @@ -90,7 +54,10 @@ def [](name) # The cookie param value. # # @option options [String] :domain - # The domain the cookie belongs to. + # The domain the cookie belongs to, the `:base_url` host by default. + # + # @option options [String] :url + # The URL to derive the domain and path from instead. # # @option options [String] :path # The path that the cookie is bound to. @@ -98,9 +65,6 @@ def [](name) # @option options [Integer] :expires # When the cookie will expire. # - # @option options [Integer] :size - # The size of the cookie. - # # @option options [Boolean] :httponly # Specifies whether the cookie `HttpOnly`. # @@ -110,8 +74,9 @@ def [](name) # @option options [String] :samesite # Specifies whether the cookie is `SameSite`. # - # @option options [Boolean] :session - # Specifies whether the cookie is a session cookie. + # @return [true] + # + # @raise [BrowserError] if neither `:domain`, `:url` nor `:base_url` is set. # # @example # browser.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true @@ -121,23 +86,16 @@ def [](name) # browser.cookies.set(nid_cookie) # => true # def set(options) - cookie = ( - options.is_a?(Cookie) ? options.attributes : options - ).dup.transform_keys(&:to_sym) - - cookie[:domain] ||= default_domain - - cookie[:httpOnly] = cookie.delete(:httponly) if cookie.key?(:httponly) - cookie[:sameSite] = cookie.delete(:samesite) if cookie.key?(:samesite) - - expires = cookie.delete(:expires).to_i - cookie[:expires] = expires if expires.positive? + cookie = cookie_params(options) + cookie[:domain] ||= default_domain unless cookie[:url] - @page.command("Network.setCookie", **cookie)["success"] + write(cookie) + true end # - # Removes given cookie. + # Removes the cookies with the given name, on the given domain or the ones + # sent to the given URL. # # @param [String] name # @@ -145,25 +103,36 @@ def set(options) # Additional keyword arguments. # # @option options [String] :domain + # The exact domain of the cookie, the `:base_url` host by default. # # @option options [String] :url + # The URL the cookie is sent to. + # + # @option options [String] :path + # The exact path of the cookie. + # + # @return [true] # # @example # browser.cookies.remove(name: "stealth", domain: "google.com") # => true # def remove(name:, **options) - raise "Specify :domain or :url option" if !options[:domain] && !options[:url] && !default_domain - - options = options.merge(name: name) - options[:domain] ||= default_domain - - @page.command("Network.deleteCookies", **options) + domain = options[:domain] || (default_domain unless options[:url]) + raise "Specify :domain or :url option" if !domain && !options[:url] + + uri = URI.parse(options[:url]) if options[:url] + cookies = select do |cookie| + cookie.name == name && + (options[:path].nil? || cookie.path == options[:path]) && + (domain ? cookie.domain == domain : sent_to?(cookie, uri)) + end + cookies.each { |cookie| write(cookie.attributes.merge("expires" => 1)) } true end # - # Removes all cookies for current page. + # Removes all cookies in the context. # # @return [true] # @@ -171,12 +140,12 @@ def remove(name:, **options) # browser.cookies.clear # => true # def clear - @page.command("Network.clearBrowserCookies") + @client.command("Storage.clearCookies", browserContextId: @context_id) true end # - # Stores all cookies of current page in a file. + # Stores all cookies in the context in a file. # # @return [Integer] # @@ -188,7 +157,7 @@ def store(path = "cookies.yml") end # - # Loads all cookies from the file and sets them for current page. + # Loads all cookies from the file and sets them in the context. # # @return [true] # @@ -203,8 +172,19 @@ def load(path = "cookies.yml") private + def write(cookie) + @client.command("Storage.setCookies", cookies: [cookie], browserContextId: @context_id) + end + def default_domain - URI.parse(@page.base_url).host if @page.base_url + base_domain(@client.options.base_url) + end + + def sent_to?(cookie, uri) + domain = cookie.domain + host_match = uri.host == domain.delete_prefix(".") || (domain.start_with?(".") && uri.host.end_with?(domain)) + path = uri.path.to_s.empty? ? "/" : uri.path + host_match && path.start_with?(cookie.path.to_s) end end end diff --git a/lib/ferrum/cookies/collection.rb b/lib/ferrum/cookies/collection.rb new file mode 100644 index 00000000..e3653c66 --- /dev/null +++ b/lib/ferrum/cookies/collection.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Ferrum + class Cookies + # + # Reading cookies by name, shared by {Cookies} and {Page::Cookies}, both + # of which define `each`. + # + module Collection + include Enumerable + + # + # @deprecated Will be removed in Ferrum 0.22, it keeps one cookie per + # name. Use `to_a`, or `map(&:name)` for the names. + # + # Returns cookies hash. + # + # @param [Hash] options + # Passed to `each`. + # + # @return [Hash{String => Cookie}] + # + # @example + # page.cookies.all # => { + # # "NID" => #"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", + # # "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false + # # }> + # # } + # + def all(**options) + Utils::Deprecate.warn( + "cookies.all will be removed in Ferrum 0.22, it keeps one cookie per name. " \ + "Use cookies.to_a, or cookies.map(&:name) for the names.", + user_code: true + ) + each(**options).to_h do |cookie| + [cookie.name, cookie] + end + end + + # + # Returns the first cookie with the given name, narrowed down by domain + # and path when the name is used by more than one cookie. + # + # @param [String] name + # The cookie name to fetch. + # + # @param [String, nil] domain + # The exact domain of the cookie, e.g. `".example.com"` for one sent to + # every subdomain. + # + # @param [String, nil] path + # The exact path of the cookie. + # + # @return [Cookie, nil] + # The matching cookie. + # + # @example + # page.cookies["NID"] # => + # # "NID", "value"=>"...", "domain"=>".google.com", + # # "path"=>"/", "expires"=>1583211046.575681, "size"=>178, + # # "httpOnly"=>true, "secure"=>false, "session"=>false + # # }> + # + # @example + # page.cookies["id", domain: "www.example.com", path: "/app"] + # + def [](name, domain: nil, path: nil) + find do |cookie| + cookie.name == name && (domain.nil? || cookie.domain == domain) && (path.nil? || cookie.path == path) + end + end + + private + + def cookie_params(options) + cookie = (options.is_a?(Cookie) ? options.attributes : options).dup.transform_keys(&:to_sym) + + cookie[:httpOnly] = cookie.delete(:httponly) if cookie.key?(:httponly) + cookie[:sameSite] = cookie.delete(:samesite) if cookie.key?(:samesite) + + expires = cookie.delete(:expires).to_i + cookie[:expires] = expires if expires.positive? + + cookie + end + + def base_domain(base_url) + URI.parse(base_url).host if base_url + end + end + end +end diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index e789c116..24347eee 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -18,6 +18,7 @@ require "ferrum/page/animation" require "ferrum/page/tracing" require "ferrum/page/stream" +require "ferrum/page/cookies" module Ferrum # @@ -84,9 +85,9 @@ class Page # @return [Headers] attr_reader :headers - # Cookie store. + # The cookies the page sees, `context.cookies` has all of the context's. # - # @return [Cookies] + # @return [Page::Cookies] attr_reader :cookies # Downloads object. diff --git a/lib/ferrum/page/cookies.rb b/lib/ferrum/page/cookies.rb new file mode 100644 index 00000000..d17f9161 --- /dev/null +++ b/lib/ferrum/page/cookies.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module Ferrum + class Page + # + # The cookies one page sees: the ones sent to its URL and the URLs of its + # frames, via the CDP `Network` domain. The page shares them with every + # other page in its {Context}, `context.cookies` has all of them whatever + # their domain. + # + class Cookies + include Ferrum::Cookies::Collection + + def initialize(page) + @page = page + end + + # + # Enumerates over the page's cookies. + # + # @param [Array, nil] urls + # The URLs to get the cookies for instead of the page's and its frames'. + # + # @yield [cookie] + # The given block will be passed each cookie. + # + # @yieldparam [Ferrum::Cookies::Cookie] cookie + # + # @return [Enumerator] + # If no block is given, an Enumerator object will be returned. + # + # @example + # page.cookies.each { |cookie| puts cookie.name } + # page.cookies.each(urls: ["https://accounts.google.com"]).map(&:name) # => ["SID"] + # + def each(urls: nil) + return enum_for(__method__, urls: urls) unless block_given? + + params = { urls: urls } if urls + cookies = @page.command("Network.getCookies", **Hash(params))["cookies"] + + cookies.each do |c| + yield Ferrum::Cookies::Cookie.new(c) + end + end + + # + # Sets a cookie, see {Ferrum::Cookies#set} for the options. Without + # `:domain` or `:url` it's set for the page's host, or the `:base_url` + # host before the page has loaded anything. + # + # @param [Hash{Symbol => Object}, Ferrum::Cookies::Cookie] options + # + # @return [Boolean] + # + # @example + # page.cookies.set(name: "stealth", value: "omg") # => true + # + def set(options) + cookie = cookie_params(options) + cookie[:domain] ||= default_domain unless cookie[:url] + + @page.command("Network.setCookie", **cookie)["success"] + end + + # + # Removes the cookies with the given name, on the page's host unless + # `:domain` or `:url` is given. + # + # @param [String] name + # + # @param [Hash{Symbol => Object}] options + # + # @option options [String] :domain + # The exact domain of the cookie. + # + # @option options [String] :url + # The URL the cookie is sent to. + # + # @option options [String] :path + # The exact path of the cookie. + # + # @return [true] + # + # @example + # page.cookies.remove(name: "stealth") # => true + # + def remove(name:, **options) + raise "Specify :domain or :url option" if !options[:domain] && !options[:url] && !default_domain + + options = options.merge(name: name) + options[:domain] ||= default_domain unless options[:url] + + @page.command("Network.deleteCookies", **options) + + true + end + + # + # Removes the page's cookies, the ones {#each} yields. Cookies on other + # domains stay, `context.cookies.clear` removes them too. + # + # @return [true] + # + # @example + # page.cookies.clear # => true + # + def clear + each do |cookie| + @page.command("Network.deleteCookies", name: cookie.name, domain: cookie.domain, path: cookie.path) + end + + true + end + + private + + def default_domain + uri = URI.parse(@page.url.to_s) + %w[http https].include?(uri.scheme) ? uri.host : base_domain(@page.base_url) + end + end + end +end diff --git a/sig/ferrum/context.rbs b/sig/ferrum/context.rbs index a75838d1..896f0a75 100644 --- a/sig/ferrum/context.rbs +++ b/sig/ferrum/context.rbs @@ -12,6 +12,7 @@ module Ferrum @targets: ::Concurrent::Hash[String, Target] @pendings: ::Concurrent::Map[String, ::Concurrent::IVar] @default_target: Target? + @cookies: Cookies def initialize: (Client, Contexts, String, ?implicit: bool) -> void @@ -19,6 +20,8 @@ module Ferrum def default_target: () -> Target + def cookies: () -> Cookies + def page: () -> Page def pages: () -> ::Array[Page] diff --git a/sig/ferrum/cookies.rbs b/sig/ferrum/cookies.rbs index b8b07ad3..81d0c134 100644 --- a/sig/ferrum/cookies.rbs +++ b/sig/ferrum/cookies.rbs @@ -1,21 +1,18 @@ module Ferrum class Cookies - include Enumerable[Cookies::Cookie] + include Cookies::Collection - @page: Page + @client: Client + @context_id: String - def initialize: (Page page) -> void + def initialize: (Client client, String context_id) -> void def each: () { (Cookies::Cookie) -> void } -> void | () -> Enumerator[Cookies::Cookie, void] - def all: () -> ::Hash[String, Cookies::Cookie] + def set: ((Cookies::Cookie | Hash[Symbol | String, untyped]) options) -> true - def []: (String name) -> Cookies::Cookie? - - def set: ((Cookies::Cookie | Hash[Symbol, untyped]) options) -> bool - - def remove: (name: String, ?domain: String?, ?url: String?) -> true + def remove: (name: String, ?domain: String?, ?url: String?, ?path: String?) -> true def clear: () -> true @@ -25,6 +22,10 @@ module Ferrum private + def write: (Hash[Symbol | String, untyped] cookie) -> Hash[String, untyped] + def default_domain: () -> String? + + def sent_to?: (Cookies::Cookie cookie, URI::Generic uri) -> bool end end diff --git a/sig/ferrum/cookies/collection.rbs b/sig/ferrum/cookies/collection.rbs new file mode 100644 index 00000000..6399d0c3 --- /dev/null +++ b/sig/ferrum/cookies/collection.rbs @@ -0,0 +1,17 @@ +module Ferrum + class Cookies + module Collection : _Each[Cookies::Cookie] + include Enumerable[Cookies::Cookie] + + %a{deprecated} def all: (**untyped options) -> ::Hash[String, Cookies::Cookie] + + def []: (String name, ?domain: String?, ?path: String?) -> Cookies::Cookie? + + private + + def cookie_params: (Cookies::Cookie | Hash[Symbol | String, untyped] options) -> Hash[Symbol, untyped] + + def base_domain: (Addressable::URI? base_url) -> String? + end + end +end diff --git a/sig/ferrum/page.rbs b/sig/ferrum/page.rbs index f5de7d27..3c0d41f8 100644 --- a/sig/ferrum/page.rbs +++ b/sig/ferrum/page.rbs @@ -22,7 +22,7 @@ module Ferrum attr_reader console: Console attr_reader accessibility: Accessibility attr_reader headers: Headers - attr_reader cookies: Cookies + attr_reader cookies: Page::Cookies attr_reader downloads: Downloads attr_reader document_version: ::Integer attr_reader client: (Client | SessionClient) @@ -37,7 +37,7 @@ module Ferrum @mouse: Mouse @keyboard: Keyboard @headers: Headers - @cookies: Cookies + @cookies: Page::Cookies @network: Network @console: Console @tracing: Page::Tracing diff --git a/sig/ferrum/page/cookies.rbs b/sig/ferrum/page/cookies.rbs new file mode 100644 index 00000000..29a64429 --- /dev/null +++ b/sig/ferrum/page/cookies.rbs @@ -0,0 +1,24 @@ +module Ferrum + class Page + class Cookies + include Ferrum::Cookies::Collection + + @page: Page + + def initialize: (Page page) -> void + + def each: (?urls: ::Array[String]?) { (Ferrum::Cookies::Cookie) -> void } -> void + | (?urls: ::Array[String]?) -> Enumerator[Ferrum::Cookies::Cookie, void] + + def set: ((Ferrum::Cookies::Cookie | Hash[Symbol | String, untyped]) options) -> bool + + def remove: (name: String, ?domain: String?, ?url: String?, ?path: String?) -> true + + def clear: () -> true + + private + + def default_domain: () -> String? + end + end +end diff --git a/spec/cookies_spec.rb b/spec/cookies_spec.rb index 3319f05f..75b61d67 100644 --- a/spec/cookies_spec.rb +++ b/spec/cookies_spec.rb @@ -55,11 +55,18 @@ end describe "#all" do - it "returns cookie object" do - browser.go_to("/set_cookie") + around do |example| + Ferrum::Utils::Deprecate.reset! + example.run + Ferrum::Utils::Deprecate.reset! + end - cookies = browser.cookies.all + it "returns cookies by name and warns that it is deprecated" do + browser.go_to("/set_cookie") + cookies = nil + expect { cookies = browser.cookies.all } + .to output(/cookies\.all will be removed in Ferrum 0\.22.*called from #{__FILE__}/m).to_stderr expect(cookies).to eq({ "stealth" => Ferrum::Cookies::Cookie.new("name" => "stealth", "value" => "test_cookie", "domain" => "127.0.0.1", @@ -75,6 +82,24 @@ end end + describe "context scope" do + it "returns the cookies of every domain in the context" do + browser.cookies.set(name: "stealth", value: "local", domain: "127.0.0.1") + browser.cookies.set(name: "other", value: "elsewhere", domain: "localhost") + + expect(browser.cookies.map(&:name)).to contain_exactly("stealth", "other") + end + + it "keeps the cookies of other contexts apart" do + browser.cookies.set(name: "stealth", value: "local") + context = browser.contexts.create + + expect(context.cookies.to_a).to eq([]) + ensure + context&.dispose + end + end + describe "#[]" do it "returns cookie object" do browser.go_to("/set_cookie") @@ -90,6 +115,23 @@ expect(cookie.session?).to be true expect(cookie.expires).to be_nil end + + it "narrows the cookies with the same name down by domain" do + browser.cookies.set(name: "stealth", value: "local", domain: "127.0.0.1") + browser.cookies.set(name: "stealth", value: "elsewhere", domain: "localhost") + + expect(browser.cookies["stealth", domain: "localhost"].value).to eq("elsewhere") + expect(browser.cookies["stealth", domain: "127.0.0.1"].value).to eq("local") + expect(browser.cookies["stealth", domain: "example.com"]).to be_nil + end + + it "narrows the cookies with the same name down by path" do + browser.cookies.set(name: "stealth", value: "root") + browser.cookies.set(name: "stealth", value: "custom", path: "/custom") + + expect(browser.cookies["stealth", path: "/custom"].value).to eq("custom") + expect(browser.cookies["stealth", path: "/"].value).to eq("root") + end end describe "#set" do @@ -244,6 +286,15 @@ browser.go_to("/get_cookie") expect(browser.body).to_not include("test_cookie") end + + it "removes the cookie sent to the given URL" do + browser.cookies.set(name: "stealth", value: "local", domain: "127.0.0.1") + browser.cookies.set(name: "stealth", value: "elsewhere", domain: "localhost") + + browser.cookies.remove(name: "stealth", url: base_url("/get_cookie")) + + expect(browser.cookies.map(&:domain)).to eq(["localhost"]) + end end describe "#clear" do @@ -262,9 +313,9 @@ describe "#store" do it "stores cookies" do - page.go_to("/set_cookie") + browser.go_to("/set_cookie") - page.cookies.store("test.yml") + browser.cookies.store("test.yml") cookies = YAML.load_file("test.yml") expect(cookies.size).to eq(1) @@ -280,10 +331,10 @@ it "stores cookies" do cookie = { "name" => "stealth", "value" => "hello world", "domain" => "127.0.0.1", "path" => "/" } File.write("test.yml", [cookie].to_yaml) - page.cookies.load("test.yml") + browser.cookies.load("test.yml") - page.go_to("/get_cookie") - expect(page.body).to include("hello world") + browser.go_to("/get_cookie") + expect(browser.body).to include("hello world") ensure File.delete("test.yml") end diff --git a/spec/page/cookies_spec.rb b/spec/page/cookies_spec.rb new file mode 100644 index 00000000..a44111d3 --- /dev/null +++ b/spec/page/cookies_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +describe Ferrum::Page::Cookies do + before do + browser.cookies.set(name: "stealth", value: "local", domain: "127.0.0.1") + browser.cookies.set(name: "other", value: "elsewhere", domain: "localhost") + end + + describe "#each" do + it "returns only the cookies sent to the page" do + page.go_to("/get_cookie") + + expect(page.cookies.map(&:name)).to eq(["stealth"]) + end + + it "returns nothing before the page has loaded a URL" do + expect(page.cookies.to_a).to eq([]) + end + + it "returns the cookies sent to the given URLs" do + page.go_to("/get_cookie") + + expect(page.cookies.each(urls: [base_url.sub("127.0.0.1", "localhost")]).map(&:name)).to eq(["other"]) + end + end + + describe "#set" do + it "sets the cookie for the page's host" do + page.go_to("http://localhost:#{server.port}/get_cookie") + + page.cookies.set(name: "stealth", value: "on localhost") + + domains = browser.cookies.select { |c| c.name == "stealth" }.map(&:domain) + expect(domains).to contain_exactly("127.0.0.1", "localhost") + end + end + + describe "#remove" do + it "removes the cookie on the page's host" do + page.go_to("/get_cookie") + + page.cookies.remove(name: "stealth") + + expect(browser.cookies.map(&:name)).to eq(["other"]) + end + end + + describe "#clear" do + it "keeps the cookies of other domains" do + page.go_to("/get_cookie") + + page.cookies.clear + + page.go_to("/get_cookie") + expect(page.body).not_to include("local") + expect(browser.cookies.map(&:name)).to eq(["other"]) + end + end +end