@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Console\Commands\ProcessArtistQueue;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
protected $commands = [
|
||||
ProcessArtistQueue::class,
|
||||
];
|
||||
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
$schedule->command('app:process-artist-queue')->everyMinute()->withoutOverlapping();
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import os
|
||||
from redis import Redis
|
||||
redis = Redis(host='redis', port=6379)
|
||||
BASE_URL = 'https://www.youtube.com'
|
||||
QUERY_URL = BASE_URL + '/results?search_query='
|
||||
|
||||
CWD = os.getcwd()
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
MEDIA_FOLDER = os.path.join(ROOT_DIR, 'music')
|
||||
|
||||
ALBUM_CONTAINER_ID = 'shelf-container'
|
||||
ALBUM_CONTAINER_CLASS = 'ytd-search-refinement-card-renderer'
|
||||
ALBUM_CONTAINER_ITEMS_XPATH = '/html/body/ytd-app/div[1]/ytd-page-manager/ytd-search/div[1]/ytd-two-column-search-results-renderer/ytd-secondary-search-container-renderer/div/ytd-universal-watch-card-renderer/div[4]/ytd-watch-card-section-sequence-renderer[2]/div/ytd-horizontal-card-list-renderer/div[2]/div[2]'
|
||||
# ALBUM_CONTAINER_FULL_XPATH = '/html/body/ytd-app/div[1]/ytd-page-manager/ytd-search/div[1]/ytd-two-column-search-results-renderer/ytd-secondary-search-container-renderer/div/ytd-universal-watch-card-renderer/div[4]/ytd-watch-card-section-sequence-renderer[2]/div'
|
||||
ALBUM_CONTAINER_FULL_XPATH = '/html/body/ytd-app/div[1]/ytd-page-manager/ytd-search/div[1]/ytd-two-column-search-results-renderer/ytd-secondary-search-container-renderer/div/ytd-universal-watch-card-renderer/div[4]/ytd-watch-card-section-sequence-renderer[2]/div/ytd-horizontal-card-list-renderer/div[2]'
|
||||
|
||||
BTN_RIGHT_FULL_XPATH = '/html/body/ytd-app/div[1]/ytd-page-manager/ytd-search/div[1]/ytd-two-column-search-results-renderer/ytd-secondary-search-container-renderer/div/ytd-universal-watch-card-renderer/div[4]/ytd-watch-card-section-sequence-renderer[2]/div/ytd-horizontal-card-list-renderer/div[2]/div[3]/div[2]/ytd-button-renderer/yt-button-shape/button'
|
||||
click_script = """
|
||||
document.evaluate('%s', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click();
|
||||
""" % BTN_RIGHT_FULL_XPATH
|
||||
@ -1,89 +0,0 @@
|
||||
import json
|
||||
import operator as oprtr
|
||||
from const import *
|
||||
from pysondb import PysonDB
|
||||
|
||||
|
||||
def evaluate_condition(record_field, operator, condition):
|
||||
return operator(record_field, condition)
|
||||
|
||||
|
||||
def evaluate_operator(op):
|
||||
if op == '>':
|
||||
op = oprtr.gt
|
||||
elif op == '<':
|
||||
op = oprtr.lt
|
||||
elif op == '=':
|
||||
op = oprtr.eq
|
||||
elif op == '!=':
|
||||
op = oprtr.ne
|
||||
else:
|
||||
raise UserWarning('Invalid Operator: %s' % op)
|
||||
return op
|
||||
|
||||
|
||||
class Model:
|
||||
# TODO: Modify some of this to be wrapped into an ENV wrapper that gets loaded in when the server starts and creates
|
||||
# class objects that can be manipulated easier by things like update_by_id
|
||||
|
||||
def __init__(self, name):
|
||||
self.env = PysonDB(CWD + '/database/%s.json' % name)
|
||||
|
||||
def _search(self, records, params):
|
||||
"""
|
||||
Iterate through list of condition tuples and append results to a checklist that will evaluate at the end
|
||||
ex params: [('name', '=', 'John'), ('zip', '!=', '12345')]
|
||||
:param params: List of tuples
|
||||
:return: Record to search recordset if True
|
||||
"""
|
||||
filtered_record_ids =[]
|
||||
for record in records:
|
||||
record_id = self.env.get_by_id(record)
|
||||
checklist = []
|
||||
for param in params:
|
||||
field = param[0]
|
||||
operator = evaluate_operator(param[1])
|
||||
condition = param[2]
|
||||
checklist.append(evaluate_condition(record_id[field], operator, condition))
|
||||
|
||||
passed = all(x for x in checklist)
|
||||
if passed:
|
||||
record_id.update({'id': record})
|
||||
filtered_record_ids.append(record_id)
|
||||
|
||||
return filtered_record_ids
|
||||
|
||||
def search(self, params):
|
||||
"""
|
||||
:param params: List of tuples that will be evaluated and return a total list of records
|
||||
:return: None, List or Single record
|
||||
"""
|
||||
records = self.env.get_all()
|
||||
record_ids = self._search(records, params)
|
||||
if not record_ids:
|
||||
record_ids = None
|
||||
|
||||
return record_ids
|
||||
|
||||
def read(self, record_id):
|
||||
data = self.env.get_by_id(record_id)
|
||||
return data
|
||||
|
||||
def create(self, vals):
|
||||
record = self.env.add(vals)
|
||||
return record
|
||||
|
||||
def create_many(self, record_list):
|
||||
record_ids = self.env.add_many(record_list)
|
||||
return record_ids
|
||||
|
||||
def write(self, record_id, vals):
|
||||
record = self.env.update_by_id(record_id, vals)
|
||||
return record
|
||||
|
||||
def unlink(self, record_id):
|
||||
self.env.delete_by_id(record_id)
|
||||
return True
|
||||
|
||||
def purge(self):
|
||||
self.env.purge()
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"keys": [
|
||||
"album",
|
||||
"artist",
|
||||
"cover",
|
||||
"downloaded",
|
||||
"downloading",
|
||||
"link"
|
||||
],
|
||||
"data": {}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
[target.i686-pc-windows-gnu]
|
||||
linker = "i686-w64-mingw32-gcc"
|
||||
rustflags = "-C panic=abort"
|
||||
@ -1,2 +0,0 @@
|
||||
Please see our contributor documentation at
|
||||
https://firefox-source-docs.mozilla.org/testing/geckodriver/#for-developers.
|
||||
@ -1,51 +0,0 @@
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "geckodriver"
|
||||
version = "0.33.0"
|
||||
authors = ["Mozilla"]
|
||||
include = [
|
||||
"/.cargo",
|
||||
"/build.rs",
|
||||
"/src"
|
||||
]
|
||||
description = "Proxy for using WebDriver clients to interact with Gecko-based browsers."
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"firefox",
|
||||
"httpd",
|
||||
"mozilla",
|
||||
"w3c",
|
||||
"webdriver",
|
||||
]
|
||||
license = "MPL-2.0"
|
||||
repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.13"
|
||||
chrono = "0.4.6"
|
||||
clap = { version = "~3.1", default-features = false, features = ["cargo", "std", "suggestions", "wrap_help"] }
|
||||
hyper = "0.14"
|
||||
lazy_static = "1.0"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
marionette = "0.4.0"
|
||||
mozdevice = "0.5.1"
|
||||
mozprofile = "0.9.1"
|
||||
mozrunner = "0.15.1"
|
||||
mozversion = "0.5.1"
|
||||
regex = { version="1.0", default-features = false, features = ["perf", "std"] }
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
tempfile = "3"
|
||||
unicode-segmentation = "1.9"
|
||||
url = "2.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
webdriver = "0.48.0"
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[[bin]]
|
||||
name = "geckodriver"
|
||||
@ -1,31 +0,0 @@
|
||||
## System
|
||||
|
||||
* Version: <!-- geckodriver version -->
|
||||
* Platform: <!-- e.g. Linux/macOS/Windows + version -->
|
||||
* Firefox: <!-- from the about dialogue -->
|
||||
* Selenium: <!-- client + version -->
|
||||
|
||||
|
||||
## Testcase
|
||||
|
||||
<!--
|
||||
Please provide a minimal HTML document which permits the problem
|
||||
to be reproduced.
|
||||
-->
|
||||
|
||||
|
||||
## Stacktrace
|
||||
|
||||
<!--
|
||||
Error and stacktrace produced by client.
|
||||
-->
|
||||
|
||||
|
||||
## Trace-level log
|
||||
|
||||
<!--
|
||||
See https://searchfox.org/mozilla-central/source/testing/geckodriver/doc/TraceLogs.md
|
||||
for how to produce a trace-level log.
|
||||
|
||||
For trace logs with more than 20 lines please add its contents as attachment.
|
||||
-->
|
||||
@ -1,385 +0,0 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at <http://mozilla.org/MPL/2.0/>.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@ -1,85 +0,0 @@
|
||||
geckodriver
|
||||
===========
|
||||
|
||||
Proxy for using W3C [WebDriver] compatible clients to interact with
|
||||
Gecko-based browsers.
|
||||
|
||||
This program provides the HTTP API described by the [WebDriver
|
||||
protocol] to communicate with Gecko browsers, such as Firefox. It
|
||||
translates calls into the [Marionette remote protocol] by acting
|
||||
as a proxy between the local- and remote ends.
|
||||
|
||||
[WebDriver protocol]: https://w3c.github.io/webdriver/#protocol
|
||||
[Marionette remote protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/
|
||||
[WebDriver]: https://developer.mozilla.org/en-US/docs/Web/WebDriver
|
||||
|
||||
|
||||
Downloads
|
||||
---------
|
||||
|
||||
* [Releases](https://github.com/mozilla/geckodriver/releases/latest)
|
||||
* [Change log](https://searchfox.org/mozilla-central/source/testing/geckodriver/CHANGES.md)
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
* [WebDriver] (work in progress)
|
||||
* [Commands](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands)
|
||||
* [Errors](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors)
|
||||
* [Types](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Types)
|
||||
|
||||
* [Cross browser testing](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing)
|
||||
|
||||
* [Selenium](https://seleniumhq.github.io/docs/) (work in progress)
|
||||
* [C# API](https://seleniumhq.github.io/selenium/docs/api/dotnet/)
|
||||
* [JavaScript API](https://seleniumhq.github.io/selenium/docs/api/javascript/)
|
||||
* [Java API](https://seleniumhq.github.io/selenium/docs/api/java/)
|
||||
* [Perl API](https://metacpan.org/pod/Selenium::Remote::Driver)
|
||||
* [Python API](https://seleniumhq.github.io/selenium/docs/api/py/)
|
||||
* [Ruby API](https://seleniumhq.github.io/selenium/docs/api/rb/)
|
||||
|
||||
* [geckodriver usage](https://firefox-source-docs.mozilla.org/testing/geckodriver/Usage.html)
|
||||
* [Supported platforms](https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html)
|
||||
* [Firefox capabilities](https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html)
|
||||
* [Capabilities example](https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#capabilities-example)
|
||||
* [Enabling trace logs](https://firefox-source-docs.mozilla.org/testing/geckodriver/TraceLogs.html)
|
||||
* [Analyzing crash data from Firefox](https://firefox-source-docs.mozilla.org/testing/geckodriver/CrashReports.html)
|
||||
|
||||
* [Contributing](https://firefox-source-docs.mozilla.org/testing/geckodriver/#for-developers)
|
||||
* [Building](https://firefox-source-docs.mozilla.org/testing/geckodriver/Building.html)
|
||||
* [Testing](https://firefox-source-docs.mozilla.org/testing/geckodriver/Testing.html)
|
||||
* [Releasing](https://firefox-source-docs.mozilla.org/testing/geckodriver/Releasing.html)
|
||||
* [Self-serving an ARM build](https://firefox-source-docs.mozilla.org/testing/geckodriver/ARM.html)
|
||||
|
||||
|
||||
Source code
|
||||
-----------
|
||||
|
||||
geckodriver is made available under the [Mozilla Public License].
|
||||
|
||||
Its source code can be found in [mozilla-central] under testing/geckodriver.
|
||||
This GitHub repository is only used for issue tracking and making releases.
|
||||
|
||||
[source code]: https://hg.mozilla.org/mozilla-unified/file/tip/testing/geckodriver
|
||||
[Mozilla Public License]: https://www.mozilla.org/en-US/MPL/2.0/
|
||||
[mozilla-central]: https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver
|
||||
|
||||
Custom release builds
|
||||
---------------------
|
||||
|
||||
If a binary is not available for your platform, it's possibe to create a custom
|
||||
build using the [Rust] toolchain. To do this, checkout the release tag for the
|
||||
version of interest and run `cargo build`. Alternatively the latest version may
|
||||
be built and installed from `crates.io` using `cargo install geckodriver`.
|
||||
|
||||
[Rust]: https://rustup.rs/
|
||||
|
||||
Contact
|
||||
-------
|
||||
|
||||
The mailing list for geckodriver discussion is
|
||||
https://groups.google.com/a/mozilla.org/g/dev-webdriver.
|
||||
|
||||
There is also an Element channel to talk about using and developing
|
||||
geckodriver on `#webdriver:mozilla.org <https://chat.mozilla.org/#/room/#webdriver:mozilla.org>`__
|
||||
@ -1,136 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Writes build information to ${OUT_DIR}/build-info.rs which is included in
|
||||
// the program during compilation:
|
||||
//
|
||||
// ```no_run
|
||||
// const COMMIT_HASH: Option<&'static str> = Some("c31a366");
|
||||
// const COMMIT_DATE: Option<&'static str> = Some("1988-05-10");
|
||||
// ```
|
||||
//
|
||||
// The values are `None` if running hg failed, e.g. if it is not installed or
|
||||
// if we are not in an hg repo.
|
||||
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let cur_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let build_info = get_build_info(&cur_dir);
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let mut fh = File::create(out_dir.join("build-info.rs"))?;
|
||||
writeln!(
|
||||
fh,
|
||||
"const COMMIT_HASH: Option<&'static str> = {:?};",
|
||||
build_info.hash()
|
||||
)?;
|
||||
writeln!(
|
||||
fh,
|
||||
"const COMMIT_DATE: Option<&'static str> = {:?};",
|
||||
build_info.date()
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_build_info(dir: &Path) -> Box<dyn BuildInfo> {
|
||||
if Path::exists(&dir.join(".hg")) {
|
||||
Box::new(Hg {})
|
||||
} else if Path::exists(&dir.join(".git")) {
|
||||
Box::new(Git {})
|
||||
} else if let Some(parent) = dir.parent() {
|
||||
get_build_info(parent)
|
||||
} else {
|
||||
eprintln!("unable to detect vcs");
|
||||
Box::new(Noop {})
|
||||
}
|
||||
}
|
||||
|
||||
trait BuildInfo {
|
||||
fn hash(&self) -> Option<String>;
|
||||
fn date(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
struct Hg;
|
||||
|
||||
impl Hg {
|
||||
fn exec<I, S>(&self, args: I) -> Option<String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new("hg")
|
||||
.env("HGPLAIN", "1")
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|r| String::from_utf8(r.stdout).ok())
|
||||
.map(|s| s.trim_end().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildInfo for Hg {
|
||||
fn hash(&self) -> Option<String> {
|
||||
self.exec(["log", "-r.", "-T{node|short}"])
|
||||
}
|
||||
|
||||
fn date(&self) -> Option<String> {
|
||||
self.exec(["log", "-r.", "-T{date|isodate}"])
|
||||
}
|
||||
}
|
||||
|
||||
struct Git;
|
||||
|
||||
impl Git {
|
||||
fn exec<I, S>(&self, args: I) -> Option<String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new("git")
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|r| String::from_utf8(r.stdout).ok())
|
||||
.map(|s| s.trim_end().into())
|
||||
}
|
||||
|
||||
fn to_hg_sha(&self, git_sha: String) -> Option<String> {
|
||||
self.exec(["cinnabar", "git2hg", &git_sha])
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildInfo for Git {
|
||||
fn hash(&self) -> Option<String> {
|
||||
self.exec(["rev-parse", "HEAD"])
|
||||
.and_then(|sha| self.to_hg_sha(sha))
|
||||
.map(|mut s| {
|
||||
s.truncate(12);
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
fn date(&self) -> Option<String> {
|
||||
self.exec(["log", "-1", "--date=short", "--pretty=format:%cd"])
|
||||
}
|
||||
}
|
||||
|
||||
struct Noop;
|
||||
|
||||
impl BuildInfo for Noop {
|
||||
fn hash(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn date(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
# Building geckodriver
|
||||
|
||||
geckodriver is written in [Rust], a systems programming language
|
||||
from Mozilla. Crucially, it relies on the [webdriver crate] to
|
||||
provide the HTTPD and do most of the heavy lifting of marshalling
|
||||
the WebDriver protocol. geckodriver translates WebDriver [commands],
|
||||
[responses], and [errors] to the [Marionette protocol], and acts
|
||||
as a proxy between [WebDriver] and [Marionette].
|
||||
|
||||
To build geckodriver:
|
||||
|
||||
```shell
|
||||
% ./mach build testing/geckodriver
|
||||
```
|
||||
|
||||
If you use artifact builds you may build geckodriver using cargo,
|
||||
since mach in this case does not have a compile environment:
|
||||
|
||||
```shell
|
||||
% cd testing/geckodriver
|
||||
% cargo build
|
||||
…
|
||||
Compiling geckodriver v0.21.0 (file:///code/gecko/testing/geckodriver)
|
||||
…
|
||||
Finished dev [optimized + debuginfo] target(s) in 7.83s
|
||||
```
|
||||
|
||||
Because all Rust code in central shares the same cargo workspace,
|
||||
the binary will be put in the `$(topsrcdir)/target` directory.
|
||||
|
||||
You can run your freshly built geckodriver this way:
|
||||
|
||||
```shell
|
||||
% ./mach geckodriver -- --other --flags
|
||||
```
|
||||
|
||||
See [Testing](Testing.md) for how to run tests.
|
||||
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[webdriver crate]: https://crates.io/crates/webdriver
|
||||
[commands]: https://docs.rs/webdriver/newest/webdriver/command/
|
||||
[responses]: https://docs.rs/webdriver/newest/webdriver/response/
|
||||
[errors]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html
|
||||
[Marionette protocol]: /testing/marionette/Protocol.md
|
||||
[WebDriver]: https://w3c.github.io/webdriver/
|
||||
[Marionette]: /testing/marionette/index.rst
|
||||
@ -1,98 +0,0 @@
|
||||
# Firefox capabilities
|
||||
|
||||
geckodriver has a few capabilities that are specific to Firefox.
|
||||
Most of these [are documented on MDN](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions).
|
||||
|
||||
We additionally have some capabilities that largely are implementation
|
||||
concerns that normal users should not care about:
|
||||
|
||||
## `moz:debuggerAddress`
|
||||
|
||||
A boolean value to indicate if Firefox has to be started with the
|
||||
[Remote Protocol] enabled, which is a low-level debugging interface that
|
||||
implements a subset of the [Chrome DevTools Protocol] (CDP).
|
||||
|
||||
When enabled the returned `moz:debuggerAddress` capability of the `New Session`
|
||||
command is the `host:port` combination of a server that supports the following
|
||||
HTTP endpoints:
|
||||
|
||||
### GET /json/version
|
||||
|
||||
The browser version metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"Browser": "Firefox/84.0a1",
|
||||
"Protocol-Version": "1.0",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:84.0) Gecko/20100101 Firefox/84.0",
|
||||
"V8-Version": "1.0",
|
||||
"WebKit-Version": "1.0",
|
||||
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/fe507083-2960-a442-bbd7-7dfe1f111c05"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /json/list
|
||||
|
||||
A list of all available websocket targets:
|
||||
|
||||
```json
|
||||
[ {
|
||||
"description": "",
|
||||
"devtoolsFrontendUrl": null,
|
||||
"faviconUrl": "",
|
||||
"id": "ecbf9028-676a-1b40-8596-a5edc0e2875b",
|
||||
"type": "page",
|
||||
"url": "https://www.mozilla.org/en-US/",
|
||||
"browsingContextId": 29,
|
||||
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/ecbf9028-676a-1b40-8596-a5edc0e2875b"
|
||||
} ]
|
||||
```
|
||||
|
||||
The contained `webSocketDebuggerUrl` entries can be used to connect to the
|
||||
websocket and interact with the browser by using the CDP protocol.
|
||||
|
||||
[Remote Protocol]: /remote/index.rst
|
||||
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
|
||||
|
||||
## `moz:useNonSpecCompliantPointerOrigin`
|
||||
|
||||
A boolean value to indicate how the pointer origin for an action
|
||||
command will be calculated.
|
||||
|
||||
With Firefox 59 the calculation will be based on the requirements
|
||||
by the [WebDriver] specification. This means that the pointer origin
|
||||
is no longer computed based on the top and left position of the
|
||||
referenced element, but on the in-view center point.
|
||||
|
||||
To temporarily disable the WebDriver conformant behavior use `false`
|
||||
as value for this capability.
|
||||
|
||||
Please note that this capability exists only temporarily, and that
|
||||
it will be removed once all Selenium bindings can handle the new
|
||||
behavior.
|
||||
|
||||
## `moz:webdriverClick`
|
||||
|
||||
A boolean value to indicate which kind of interactability checks
|
||||
to run when performing a click or sending keys to an elements. For
|
||||
Firefoxen prior to version 58.0 some legacy code as imported from
|
||||
an older version of FirefoxDriver was in use.
|
||||
|
||||
With Firefox 58 the interactability checks as required by the
|
||||
[WebDriver] specification are enabled by default. This means
|
||||
geckodriver will additionally check if an element is obscured by
|
||||
another when clicking, and if an element is focusable for sending
|
||||
keys.
|
||||
|
||||
Because of this change in behaviour, we are aware that some extra
|
||||
errors could be returned. In most cases the test in question might
|
||||
have to be updated so it's conform with the new checks. But if the
|
||||
problem is located in geckodriver, then please raise an issue in
|
||||
the [issue tracker].
|
||||
|
||||
To temporarily disable the WebDriver conformant checks use `false`
|
||||
as value for this capability.
|
||||
|
||||
Please note that this capability exists only temporarily, and that
|
||||
it will be removed once the interactability checks have been
|
||||
stabilized.
|
||||
@ -1,67 +0,0 @@
|
||||
# Analyzing crash data of Firefox
|
||||
|
||||
It's not uncommon that under some special platform configurations and while
|
||||
running automated tests via Selenium and geckodriver Firefox could crash. In
|
||||
those cases it is very helpful to retrieve the generated crash data aka
|
||||
minidump files, and report these to us.
|
||||
|
||||
## Retrieve the crash data
|
||||
|
||||
Because geckodriver creates a temporary user profile for Firefox, it also
|
||||
automatically removes all its folders once the tests have been finished. That
|
||||
also means that if Firefox crashed the created minidump files are lost. To
|
||||
prevent that a custom profile has to be used instead. The following code
|
||||
shows an example by using the Python Selenium bindings on Mac OS:
|
||||
|
||||
```python
|
||||
import tempfile
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
|
||||
# Custom profile folder to keep the minidump files
|
||||
profile = tempfile.mkdtemp(".selenium")
|
||||
print("*** Using profile: {}".format(profile))
|
||||
|
||||
# Use the above folder as custom profile
|
||||
opts = Options()
|
||||
opts.add_argument("-profile")
|
||||
opts.add_argument(profile)
|
||||
opts.binary = "/Applications/Firefox.app/Contents/MacOS/firefox"
|
||||
|
||||
driver = webdriver.Firefox(
|
||||
options=opts,
|
||||
# hard-code the Marionette port so geckodriver can connect
|
||||
service_args=["--marionette-port", "2828"]
|
||||
)
|
||||
|
||||
# Your test code which crashes Firefox
|
||||
```
|
||||
|
||||
Executing the test with Selenium now, which triggers the crash of Firefox
|
||||
will leave all the files from the user profile around in the above path.
|
||||
|
||||
To retrieve the minidump files navigate to that folder and look for a sub
|
||||
folder with the name `minidumps`. It should contain at least one series of
|
||||
files. One file with the `.dmp` extension and another one with `.extra`.
|
||||
Both of those files are needed. If more crash files are present grab them all.
|
||||
|
||||
Attach the files as best archived as zip file to the created [geckodriver issue]
|
||||
on Github.
|
||||
|
||||
[geckodriver issue]: https://github.com/mozilla/geckodriver/issues/new
|
||||
|
||||
## Getting details of the crash
|
||||
|
||||
More advanced users can upload the generated minidump files themselves and
|
||||
receive details information about the crash. Therefore find the [crash reporter]
|
||||
folder and copy all the generated minidump files into the `pending` sub directory.
|
||||
Make sure that both the `.dmp` and `.extra` files are present.
|
||||
|
||||
Once done you can also [view the crash reports].
|
||||
|
||||
If you submitted a crash please do not forget to also add the link of the
|
||||
crash report to the geckodriver issue.
|
||||
|
||||
[crash reporter]: https://support.mozilla.org/kb/mozillacrashreporter#w_viewing-reports-outside-of-firefox
|
||||
[view the crash reports]: https://support.mozilla.orgkb/mozillacrashreporter#w_viewing-crash-reports
|
||||
@ -1,44 +0,0 @@
|
||||
# MacOS notarization
|
||||
|
||||
With the introduction of macOS 10.15 “Catalina” Apple introduced
|
||||
[new notarization requirements] that all software must be signed
|
||||
and notarized centrally.
|
||||
|
||||
Whilst the geckodriver binary is technically both signed and notarized, the
|
||||
actual validation can only be performed by MacOS if the machine that starts
|
||||
the geckodriver binary for the very first time is online. Offline validation
|
||||
would require shipping geckodriver as a DMG/PKG. You can track the relevant
|
||||
progress in [bug 1783943].
|
||||
|
||||
Note: geckodriver releases between 0.26.0 and 0.31.0 don't have the
|
||||
notarization applied and always require the manual steps below to
|
||||
bypass the notarization requirement of the binary during the very first start.
|
||||
|
||||
[new notarization requirements]: https://developer.apple.com/news/?id=04102019a
|
||||
[bug 1783943]: https://bugzilla.mozilla.org/show_bug.cgi?id=1783943
|
||||
|
||||
## Offline mode
|
||||
|
||||
There are some mitigating circumstances:
|
||||
|
||||
* Verification problems only occur when other notarized programs,
|
||||
such as a web browser, downloads the software from the internet.
|
||||
|
||||
* Arbitrary software downloaded through other means, such as
|
||||
curl(1) is _not_ affected by this change.
|
||||
|
||||
In other words, if your method for fetching geckodriver on macOS
|
||||
is through the GitHub web UI using a web browser, the program will
|
||||
not be able to run unless you manually disable the quarantine check
|
||||
(explained below). If downloading geckodriver via other means
|
||||
than a macOS notarized program, you should not be affected.
|
||||
|
||||
To bypass the notarization requirement on macOS if you have downloaded
|
||||
the geckodriver .tar.gz via a web browser, you can run the following
|
||||
command in a terminal:
|
||||
|
||||
% xattr -r -d com.apple.quarantine geckodriver
|
||||
|
||||
A problem with notarization will manifest itself through a security
|
||||
dialogue appearing, explaining that the source of the program is
|
||||
not trusted.
|
||||
@ -1,31 +0,0 @@
|
||||
# Submitting patches
|
||||
|
||||
You can submit patches by using [Phabricator]. Walk through its documentation
|
||||
in how to set it up, and uploading patches for review. Don't worry about which
|
||||
person to select for reviewing your code. It will be done automatically.
|
||||
|
||||
Please also make sure to follow the [commit creation guidelines].
|
||||
|
||||
Once you have contributed a couple of patches, we are happy to sponsor you in
|
||||
[becoming a Mozilla committer]. When you have been granted commit access
|
||||
level 1, you will have permission to use the [Firefox CI] to trigger your own
|
||||
“try runs” to test your changes. You can use the following [try preset] to run
|
||||
the most relevant tests:
|
||||
|
||||
```shell
|
||||
% ./mach try --preset geckodriver
|
||||
```
|
||||
|
||||
This preset will schedule geckodriver-related tests on various platforms. You can
|
||||
reduce the number of tasks by filtering on platforms (e.g. linux) or build type
|
||||
(e.g. opt):
|
||||
|
||||
```shell
|
||||
% ./mach try --preset geckodriver -xq "'linux 'opt"
|
||||
```
|
||||
|
||||
[Phabricator]: https://moz-conduit.readthedocs.io/en/latest/phabricator-user.html
|
||||
[commit creation guidelines]: https://mozilla-version-control-tools.readthedocs.io/en/latest/devguide/contributing.html?highlight=phabricator#submitting-patches-for-review
|
||||
[becoming a Mozilla committer]: https://www.mozilla.org/en-US/about/governance/policies/commit/
|
||||
[Firefox CI]: https://treeherder.mozilla.org/
|
||||
[try preset]: https://firefox-source-docs.mozilla.org/tools/try/presets.html
|
||||
@ -1,183 +0,0 @@
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
# Supported platforms
|
||||
|
||||
The following table shows a mapping between [geckodriver releases],
|
||||
and required versions of Selenium and Firefox:
|
||||
|
||||
<style type="text/css">
|
||||
table { width: 100%; margin-bottom: 2em; }
|
||||
table, th, td { border: solid gray 1px; }
|
||||
td, th { padding: 5px 10px; text-align: center; }
|
||||
</style>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">geckodriver
|
||||
<th rowspan="2">Selenium
|
||||
<th colspan="2">Firefox
|
||||
</tr>
|
||||
<tr>
|
||||
<th>min
|
||||
<th>max
|
||||
</tr>
|
||||
</thead>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>0.33.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>102 ESR
|
||||
<td>n/a
|
||||
<tr>
|
||||
<td>0.32.2
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>102 ESR
|
||||
<td>n/a
|
||||
<tr>
|
||||
<td>0.32.1
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>102 ESR
|
||||
<td>n/a
|
||||
<tr>
|
||||
<td>0.32.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>102 ESR
|
||||
<td>n/a
|
||||
<tr>
|
||||
<td>0.31.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>91 ESR
|
||||
<td>n/a
|
||||
<tr>
|
||||
<tr>
|
||||
<td>0.30.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>78 ESR
|
||||
<td>90
|
||||
<tr>
|
||||
<td>0.29.1
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>60
|
||||
<td>90
|
||||
<tr>
|
||||
<td>0.29.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>60
|
||||
<td>90
|
||||
<tr>
|
||||
<td>0.28.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>60
|
||||
<td>90
|
||||
<tr>
|
||||
<td>0.27.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>60
|
||||
<td>90
|
||||
<tr>
|
||||
<td>0.26.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>60
|
||||
<td>90
|
||||
<tr>
|
||||
<td>0.25.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>57
|
||||
<td>90
|
||||
<tr>
|
||||
<td>0.24.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>57
|
||||
<td>79
|
||||
<tr>
|
||||
<td>0.23.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>57
|
||||
<td>79
|
||||
<tr>
|
||||
<td>0.22.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>57
|
||||
<td>79
|
||||
<tr>
|
||||
<td>0.21.0
|
||||
<td>≥ 3.11 (3.14 Python)
|
||||
<td>57
|
||||
<td>79
|
||||
<tr>
|
||||
<td>0.20.1
|
||||
<td>≥ 3.5
|
||||
<td>55
|
||||
<td>62
|
||||
<tr>
|
||||
<td>0.20.0
|
||||
<td>≥ 3.5
|
||||
<td>55
|
||||
<td>62
|
||||
<tr>
|
||||
<td>0.19.1
|
||||
<td>≥ 3.5
|
||||
<td>55
|
||||
<td>62
|
||||
<tr>
|
||||
<td>0.19.0
|
||||
<td>≥ 3.5
|
||||
<td>55
|
||||
<td>62
|
||||
<tr>
|
||||
<td>0.18.0
|
||||
<td>≥ 3.4
|
||||
<td>53
|
||||
<td>62
|
||||
<tr>
|
||||
<td>0.17.0
|
||||
<td>≥ 3.4
|
||||
<td>52
|
||||
<td>62
|
||||
</table>
|
||||
|
||||
## Clients
|
||||
|
||||
[Selenium] users must update to version 3.11 or later to use geckodriver.
|
||||
Other clients that follow the [W3C WebDriver specification][WebDriver]
|
||||
are also supported.
|
||||
|
||||
## Firefoxen
|
||||
|
||||
geckodriver is not yet feature complete. This means that it does
|
||||
not yet offer full conformance with the [WebDriver] standard
|
||||
or complete compatibility with [Selenium]. You can track the
|
||||
[implementation status] of the latest [Firefox Nightly] on MDN.
|
||||
We also keep track of known [Selenium], [remote protocol], and
|
||||
[specification] problems in our [issue tracker].
|
||||
|
||||
Support is best in Firefox 57 and greater, although generally the more
|
||||
recent the Firefox version, the better the experience as they have
|
||||
more bug fixes and features. Some features will only be available
|
||||
in the most recent Firefox versions, and we strongly advise using the
|
||||
latest [Firefox Nightly] with geckodriver. Since Windows XP support
|
||||
in Firefox was dropped with Firefox 53, we do not support this platform.
|
||||
|
||||
## Android
|
||||
|
||||
Starting with the 0.26.0 release geckodriver is able to connect
|
||||
to Android devices, and to control packages which are based on [GeckoView]
|
||||
(eg. [Firefox Preview] aka Fenix, or [Firefox Reality]). But it also still
|
||||
supports versions of Fennec up to 68 ESR, which is the last officially
|
||||
supported release from Mozilla.
|
||||
|
||||
To run tests on Android specific capabilities under `moz:firefoxOptions`
|
||||
have to be set when requesting a new session. See the Android section under
|
||||
[Firefox Capabilities](Capabilities.md#android) for more details.
|
||||
|
||||
[geckodriver releases]: https://github.com/mozilla/geckodriver/releases
|
||||
[Selenium]: https://github.com/seleniumhq/selenium
|
||||
[WebDriver]: https://w3c.github.io/webdriver/
|
||||
[implementation status]: https://bugzilla.mozilla.org/showdependencytree.cgi?id=721859&hide_resolved=1
|
||||
[remote protocol]: https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Amarionette
|
||||
[specification]: https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Aspec
|
||||
[issue tracker]: https://github.com/mozilla/geckodriver/issues
|
||||
[Firefox Nightly]: https://nightly.mozilla.org/
|
||||
[GeckoView]: https://wiki.mozilla.org/Mobile/GeckoView
|
||||
[Firefox Preview]: https://play.google.com/store/apps/details?id=org.mozilla.fenix
|
||||
[Firefox Reality]: https://play.google.com/store/apps/details?id=org.mozilla.vrbrowser
|
||||
@ -1,55 +0,0 @@
|
||||
===========
|
||||
geckodriver
|
||||
===========
|
||||
|
||||
Proxy for using W3C WebDriver-compatible clients to interact with
|
||||
Gecko-based browsers.
|
||||
|
||||
This program provides the HTTP API described by the `WebDriver protocol`_.
|
||||
to communicate with Gecko browsers, such as Firefox. It translates calls
|
||||
into the :ref:`Firefox remote protocol <Protocol>` by acting as a proxy between the local-
|
||||
and remote ends.
|
||||
|
||||
You can consult the `change log`_ for a record of all notable changes
|
||||
to the program. Releases_ are made available on GitHub.
|
||||
|
||||
.. _WebDriver protocol: https://w3c.github.io/webdriver/#protocol
|
||||
.. _change log: https://github.com/mozilla/geckodriver/releases
|
||||
.. _Releases: https://github.com/mozilla/geckodriver/releases
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
Support.md
|
||||
WebDriver capabilities <https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities>
|
||||
Capabilities.md
|
||||
Usage.md
|
||||
Flags.md
|
||||
Profiles.md
|
||||
Bugs.md
|
||||
TraceLogs.md
|
||||
CrashReports.md
|
||||
Notarization.md
|
||||
|
||||
|
||||
For developers
|
||||
==============
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
Building.md
|
||||
Testing.md
|
||||
Patches.md
|
||||
Releasing.md
|
||||
ARM.md
|
||||
|
||||
|
||||
Communication
|
||||
=============
|
||||
|
||||
The mailing list for geckodriver discussion is
|
||||
https://groups.google.com/a/mozilla.org/g/dev-webdriver.
|
||||
|
||||
If you prefer real-time chat, ask your questions
|
||||
on `#webdriver:mozilla.org <https://chat.mozilla.org/#/room/#webdriver:mozilla.org>`__.
|
||||
@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "marionette"
|
||||
version = "0.4.0"
|
||||
authors = ["Mozilla"]
|
||||
description = "Library implementing the client side of Gecko's Marionette remote automation protocol."
|
||||
edition = "2018"
|
||||
keywords = ["mozilla", "firefox", "marionette", "webdriver"]
|
||||
license = "MPL-2.0"
|
||||
repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver/marionette"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
@ -1,240 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BoolValue {
|
||||
value: bool,
|
||||
}
|
||||
|
||||
impl BoolValue {
|
||||
pub fn new(val: bool) -> Self {
|
||||
BoolValue { value: val }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Cookie {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domain: Option<String>,
|
||||
#[serde(default)]
|
||||
pub secure: bool,
|
||||
#[serde(default, rename = "httpOnly")]
|
||||
pub http_only: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expiry: Option<Date>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", rename = "sameSite")]
|
||||
pub same_site: Option<String>,
|
||||
}
|
||||
|
||||
pub fn to_cookie<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: Serialize,
|
||||
{
|
||||
#[derive(Serialize)]
|
||||
struct Wrapper<T> {
|
||||
cookie: T,
|
||||
}
|
||||
|
||||
Wrapper { cookie: data }.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn from_cookie<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: serde::de::DeserializeOwned,
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Wrapper<T> {
|
||||
cookie: T,
|
||||
}
|
||||
|
||||
let w = Wrapper::deserialize(deserializer)?;
|
||||
Ok(w.cookie)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Date(pub u64);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Frame {
|
||||
Index(u16),
|
||||
Element(String),
|
||||
Parent,
|
||||
}
|
||||
|
||||
impl Serialize for Frame {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
match self {
|
||||
Frame::Index(nth) => map.serialize_entry("id", nth)?,
|
||||
Frame::Element(el) => map.serialize_entry("element", el)?,
|
||||
Frame::Parent => map.serialize_entry("id", &Value::Null)?,
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Frame {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Frame, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
struct JsonFrame {
|
||||
id: Option<u16>,
|
||||
element: Option<String>,
|
||||
}
|
||||
|
||||
let json = JsonFrame::deserialize(deserializer)?;
|
||||
match (json.id, json.element) {
|
||||
(Some(_id), Some(_element)) => Err(de::Error::custom("conflicting frame identifiers")),
|
||||
(Some(id), None) => Ok(Frame::Index(id)),
|
||||
(None, Some(element)) => Ok(Frame::Element(element)),
|
||||
(None, None) => Ok(Frame::Parent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(nupur): Bug 1567165 - Make WebElement in Marionette a unit struct
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WebElement {
|
||||
#[serde(rename = "element-6066-11e4-a52e-4f735466cecf")]
|
||||
pub element: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Timeouts {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub implicit: Option<u64>,
|
||||
#[serde(default, rename = "pageLoad", skip_serializing_if = "Option::is_none")]
|
||||
pub page_load: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[allow(clippy::option_option)]
|
||||
pub script: Option<Option<u64>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Window {
|
||||
pub handle: String,
|
||||
}
|
||||
|
||||
pub fn to_name<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: Serialize,
|
||||
{
|
||||
#[derive(Serialize)]
|
||||
struct Wrapper<T> {
|
||||
name: T,
|
||||
}
|
||||
|
||||
Wrapper { name: data }.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn from_name<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: serde::de::DeserializeOwned,
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Wrapper<T> {
|
||||
name: T,
|
||||
}
|
||||
|
||||
let w = Wrapper::deserialize(deserializer)?;
|
||||
Ok(w.name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::{assert_de, assert_ser, assert_ser_de, ELEMENT_KEY};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_cookie_default_values() {
|
||||
let data = Cookie {
|
||||
name: "hello".into(),
|
||||
value: "world".into(),
|
||||
path: None,
|
||||
domain: None,
|
||||
secure: false,
|
||||
http_only: false,
|
||||
expiry: None,
|
||||
same_site: None,
|
||||
};
|
||||
assert_de(&data, json!({"name":"hello", "value":"world"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_frame_index() {
|
||||
assert_ser_de(&Frame::Index(1234), json!({"id": 1234}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_frame_element() {
|
||||
assert_ser_de(&Frame::Element("elem".into()), json!({"element": "elem"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_frame_parent() {
|
||||
assert_ser_de(&Frame::Parent, json!({ "id": null }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_web_element() {
|
||||
let data = WebElement {
|
||||
element: "foo".into(),
|
||||
};
|
||||
assert_ser_de(&data, json!({ELEMENT_KEY: "foo"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeouts_with_all_params() {
|
||||
let data = Timeouts {
|
||||
implicit: Some(1000),
|
||||
page_load: Some(200000),
|
||||
script: Some(Some(60000)),
|
||||
};
|
||||
assert_ser_de(
|
||||
&data,
|
||||
json!({"implicit":1000,"pageLoad":200000,"script":60000}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeouts_with_missing_params() {
|
||||
let data = Timeouts {
|
||||
implicit: Some(1000),
|
||||
page_load: None,
|
||||
script: None,
|
||||
};
|
||||
assert_ser_de(&data, json!({"implicit":1000}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeouts_setting_script_none() {
|
||||
let data = Timeouts {
|
||||
implicit: Some(1000),
|
||||
page_load: None,
|
||||
script: Some(None),
|
||||
};
|
||||
assert_ser(&data, json!({"implicit":1000, "script":null}));
|
||||
}
|
||||
}
|
||||
@ -1,184 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum Error {
|
||||
Marionette(MarionetteError),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
match *self {
|
||||
Error::Marionette(ref err) => err.kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::Marionette(ref err) => fmt
|
||||
.debug_struct("Marionette")
|
||||
.field("kind", &err.kind)
|
||||
.field("message", &err.message)
|
||||
.field("stacktrace", &err.stack.clone())
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::Marionette(ref err) => write!(fmt, "{}: {}", err.kind, err.message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
match self {
|
||||
Error::Marionette(_) => self.kind().as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct MarionetteError {
|
||||
#[serde(rename = "error")]
|
||||
pub kind: ErrorKind,
|
||||
#[serde(default = "empty_string")]
|
||||
pub message: String,
|
||||
#[serde(rename = "stacktrace", default = "empty_string")]
|
||||
pub stack: String,
|
||||
}
|
||||
|
||||
fn empty_string() -> String {
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
impl From<MarionetteError> for Error {
|
||||
fn from(error: MarionetteError) -> Error {
|
||||
Error::Marionette(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub enum ErrorKind {
|
||||
#[serde(rename = "element click intercepted")]
|
||||
ElementClickIntercepted,
|
||||
#[serde(rename = "element not accessible")]
|
||||
ElementNotAccessible,
|
||||
#[serde(rename = "element not interactable")]
|
||||
ElementNotInteractable,
|
||||
#[serde(rename = "insecure certificate")]
|
||||
InsecureCertificate,
|
||||
#[serde(rename = "invalid argument")]
|
||||
InvalidArgument,
|
||||
#[serde(rename = "invalid cookie")]
|
||||
InvalidCookieDomain,
|
||||
#[serde(rename = "invalid element state")]
|
||||
InvalidElementState,
|
||||
#[serde(rename = "invalid selector")]
|
||||
InvalidSelector,
|
||||
#[serde(rename = "invalid session id")]
|
||||
InvalidSessionId,
|
||||
#[serde(rename = "javascript error")]
|
||||
JavaScript,
|
||||
#[serde(rename = "move target out of bounds")]
|
||||
MoveTargetOutOfBounds,
|
||||
#[serde(rename = "no such alert")]
|
||||
NoSuchAlert,
|
||||
#[serde(rename = "no such element")]
|
||||
NoSuchElement,
|
||||
#[serde(rename = "no such frame")]
|
||||
NoSuchFrame,
|
||||
#[serde(rename = "no such window")]
|
||||
NoSuchWindow,
|
||||
#[serde(rename = "script timeout")]
|
||||
ScriptTimeout,
|
||||
#[serde(rename = "session not created")]
|
||||
SessionNotCreated,
|
||||
#[serde(rename = "stale element reference")]
|
||||
StaleElementReference,
|
||||
#[serde(rename = "timeout")]
|
||||
Timeout,
|
||||
#[serde(rename = "unable to set cookie")]
|
||||
UnableToSetCookie,
|
||||
#[serde(rename = "unexpected alert open")]
|
||||
UnexpectedAlertOpen,
|
||||
#[serde(rename = "unknown command")]
|
||||
UnknownCommand,
|
||||
#[serde(rename = "unknown error")]
|
||||
Unknown,
|
||||
#[serde(rename = "unsupported operation")]
|
||||
UnsupportedOperation,
|
||||
#[serde(rename = "webdriver error")]
|
||||
WebDriver,
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
use ErrorKind::*;
|
||||
match self {
|
||||
ElementClickIntercepted => "element click intercepted",
|
||||
ElementNotAccessible => "element not accessible",
|
||||
ElementNotInteractable => "element not interactable",
|
||||
InsecureCertificate => "insecure certificate",
|
||||
InvalidArgument => "invalid argument",
|
||||
InvalidCookieDomain => "invalid cookie",
|
||||
InvalidElementState => "invalid element state",
|
||||
InvalidSelector => "invalid selector",
|
||||
InvalidSessionId => "invalid session id",
|
||||
JavaScript => "javascript error",
|
||||
MoveTargetOutOfBounds => "move target out of bounds",
|
||||
NoSuchAlert => "no such alert",
|
||||
NoSuchElement => "no such element",
|
||||
NoSuchFrame => "no such frame",
|
||||
NoSuchWindow => "no such window",
|
||||
ScriptTimeout => "script timeout",
|
||||
SessionNotCreated => "session not created",
|
||||
StaleElementReference => "stale eelement referencee",
|
||||
Timeout => "timeout",
|
||||
UnableToSetCookie => "unable to set cookie",
|
||||
UnexpectedAlertOpen => "unexpected alert open",
|
||||
UnknownCommand => "unknown command",
|
||||
Unknown => "unknown error",
|
||||
UnsupportedOperation => "unsupported operation",
|
||||
WebDriver => "webdriver error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::assert_ser_de;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_json_error() {
|
||||
let err = MarionetteError {
|
||||
kind: ErrorKind::Timeout,
|
||||
message: "".into(),
|
||||
stack: "".into(),
|
||||
};
|
||||
assert_ser_de(
|
||||
&err,
|
||||
json!({"error": "timeout", "message": "", "stacktrace": ""}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub mod common;
|
||||
pub mod marionette;
|
||||
pub mod message;
|
||||
pub mod result;
|
||||
pub mod webdriver;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
@ -1,69 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::BoolValue;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum AppStatus {
|
||||
eAttemptQuit,
|
||||
eConsiderQuit,
|
||||
eForceQuit,
|
||||
eRestart,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Command {
|
||||
#[serde(rename = "Marionette:AcceptConnections")]
|
||||
AcceptConnections(BoolValue),
|
||||
#[serde(rename = "Marionette:Quit")]
|
||||
DeleteSession { flags: Vec<AppStatus> },
|
||||
#[serde(rename = "Marionette:GetContext")]
|
||||
GetContext,
|
||||
#[serde(rename = "Marionette:GetScreenOrientation")]
|
||||
GetScreenOrientation,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::assert_ser_de;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_json_command_accept_connections() {
|
||||
assert_ser_de(
|
||||
&Command::AcceptConnections(BoolValue::new(false)),
|
||||
json!({"Marionette:AcceptConnections": {"value": false }}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_command_delete_session() {
|
||||
let data = &Command::DeleteSession {
|
||||
flags: vec![AppStatus::eForceQuit],
|
||||
};
|
||||
assert_ser_de(data, json!({"Marionette:Quit": {"flags": ["eForceQuit"]}}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_command_get_context() {
|
||||
assert_ser_de(&Command::GetContext, json!("Marionette:GetContext"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_command_get_screen_orientation() {
|
||||
assert_ser_de(
|
||||
&Command::GetScreenOrientation,
|
||||
json!("Marionette:GetScreenOrientation"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_command_invalid() {
|
||||
assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
|
||||
}
|
||||
}
|
||||
@ -1,336 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde::de::{self, SeqAccess, Unexpected, Visitor};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{Map, Value};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use std::fmt;
|
||||
|
||||
use crate::error::MarionetteError;
|
||||
use crate::marionette;
|
||||
use crate::result::MarionetteResult;
|
||||
use crate::webdriver;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Command {
|
||||
WebDriver(webdriver::Command),
|
||||
Marionette(marionette::Command),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn name(&self) -> String {
|
||||
let (command_name, _) = self.first_entry();
|
||||
command_name
|
||||
}
|
||||
|
||||
fn params(&self) -> Value {
|
||||
let (_, params) = self.first_entry();
|
||||
params
|
||||
}
|
||||
|
||||
fn first_entry(&self) -> (String, serde_json::Value) {
|
||||
match serde_json::to_value(self).unwrap() {
|
||||
Value::String(cmd) => (cmd, Value::Object(Map::new())),
|
||||
Value::Object(items) => {
|
||||
let mut iter = items.iter();
|
||||
let (cmd, params) = iter.next().unwrap();
|
||||
(cmd.to_string(), params.clone())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
enum MessageDirection {
|
||||
Incoming = 0,
|
||||
Outgoing = 1,
|
||||
}
|
||||
|
||||
pub type MessageId = u32;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Request(pub MessageId, pub Command);
|
||||
|
||||
impl Request {
|
||||
pub fn id(&self) -> MessageId {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn command(&self) -> &Command {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn params(&self) -> Value {
|
||||
self.command().params()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Request {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
(
|
||||
MessageDirection::Incoming,
|
||||
self.id(),
|
||||
self.command().name(),
|
||||
self.params(),
|
||||
)
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Response {
|
||||
Result {
|
||||
id: MessageId,
|
||||
result: MarionetteResult,
|
||||
},
|
||||
Error {
|
||||
id: MessageId,
|
||||
error: MarionetteError,
|
||||
},
|
||||
}
|
||||
|
||||
impl Serialize for Response {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Response::Result { id, result } => {
|
||||
(MessageDirection::Outgoing, id, Value::Null, &result).serialize(serializer)
|
||||
}
|
||||
Response::Error { id, error } => {
|
||||
(MessageDirection::Outgoing, id, &error, Value::Null).serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Message {
|
||||
Incoming(Request),
|
||||
Outgoing(Response),
|
||||
}
|
||||
|
||||
struct MessageVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MessageVisitor {
|
||||
type Value = Message;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("four-element array")
|
||||
}
|
||||
|
||||
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
|
||||
let direction = seq
|
||||
.next_element::<MessageDirection>()?
|
||||
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
|
||||
let id: MessageId = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
|
||||
|
||||
let msg = match direction {
|
||||
MessageDirection::Incoming => {
|
||||
let name: String = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
|
||||
let params: Value = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
|
||||
|
||||
let command = match params {
|
||||
Value::Object(ref items) if !items.is_empty() => {
|
||||
let command_to_params = {
|
||||
let mut m = Map::new();
|
||||
m.insert(name, params);
|
||||
Value::Object(m)
|
||||
};
|
||||
serde_json::from_value(command_to_params).map_err(de::Error::custom)
|
||||
}
|
||||
Value::Object(_) | Value::Null => {
|
||||
serde_json::from_value(Value::String(name)).map_err(de::Error::custom)
|
||||
}
|
||||
x => Err(de::Error::custom(format!("unknown params type: {}", x))),
|
||||
}?;
|
||||
Message::Incoming(Request(id, command))
|
||||
}
|
||||
|
||||
MessageDirection::Outgoing => {
|
||||
let maybe_error: Option<MarionetteError> = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
|
||||
|
||||
let response = if let Some(error) = maybe_error {
|
||||
seq.next_element::<Value>()?
|
||||
.ok_or_else(|| de::Error::invalid_length(3, &self))?
|
||||
.as_null()
|
||||
.ok_or_else(|| de::Error::invalid_type(Unexpected::Unit, &self))?;
|
||||
Response::Error { id, error }
|
||||
} else {
|
||||
let result: MarionetteResult = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
|
||||
Response::Result { id, result }
|
||||
};
|
||||
|
||||
Message::Outgoing(response)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Message {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_seq(MessageVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::common::*;
|
||||
use crate::error::{ErrorKind, MarionetteError};
|
||||
use crate::test::assert_ser_de;
|
||||
|
||||
#[test]
|
||||
fn test_incoming() {
|
||||
let json =
|
||||
json!([0, 42, "WebDriver:FindElement", {"using": "css selector", "value": "value"}]);
|
||||
let find_element = webdriver::Command::FindElement(webdriver::Locator {
|
||||
using: webdriver::Selector::Css,
|
||||
value: "value".into(),
|
||||
});
|
||||
let req = Request(42, Command::WebDriver(find_element));
|
||||
let msg = Message::Incoming(req);
|
||||
assert_ser_de(&msg, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incoming_empty_params() {
|
||||
let json = json!([0, 42, "WebDriver:GetTimeouts", {}]);
|
||||
let req = Request(42, Command::WebDriver(webdriver::Command::GetTimeouts));
|
||||
let msg = Message::Incoming(req);
|
||||
assert_ser_de(&msg, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incoming_common_params() {
|
||||
let json = json!([0, 42, "Marionette:AcceptConnections", {"value": false}]);
|
||||
let params = BoolValue::new(false);
|
||||
let req = Request(
|
||||
42,
|
||||
Command::Marionette(marionette::Command::AcceptConnections(params)),
|
||||
);
|
||||
let msg = Message::Incoming(req);
|
||||
assert_ser_de(&msg, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incoming_params_derived() {
|
||||
assert!(serde_json::from_value::<Message>(
|
||||
json!([0,42,"WebDriver:FindElement",{"using":"foo","value":"foo"}])
|
||||
)
|
||||
.is_err());
|
||||
assert!(serde_json::from_value::<Message>(
|
||||
json!([0,42,"Marionette:AcceptConnections",{"value":"foo"}])
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incoming_no_params() {
|
||||
assert!(serde_json::from_value::<Message>(
|
||||
json!([0,42,"WebDriver:GetTimeouts",{"value":true}])
|
||||
)
|
||||
.is_err());
|
||||
assert!(serde_json::from_value::<Message>(
|
||||
json!([0,42,"Marionette:Context",{"value":"foo"}])
|
||||
)
|
||||
.is_err());
|
||||
assert!(serde_json::from_value::<Message>(
|
||||
json!([0,42,"Marionette:GetScreenOrientation",{"value":true}])
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_outgoing_result() {
|
||||
let json = json!([1, 42, null, { "value": null }]);
|
||||
let result = MarionetteResult::Null;
|
||||
let msg = Message::Outgoing(Response::Result { id: 42, result });
|
||||
|
||||
assert_ser_de(&msg, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_outgoing_error() {
|
||||
let json =
|
||||
json!([1, 42, {"error": "no such element", "message": "", "stacktrace": ""}, null]);
|
||||
let error = MarionetteError {
|
||||
kind: ErrorKind::NoSuchElement,
|
||||
message: "".into(),
|
||||
stack: "".into(),
|
||||
};
|
||||
let msg = Message::Outgoing(Response::Error { id: 42, error });
|
||||
|
||||
assert_ser_de(&msg, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_type() {
|
||||
assert!(
|
||||
serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts", {}])).is_err()
|
||||
);
|
||||
assert!(serde_json::from_value::<Message>(json!([3, 42, "no such element", {}])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_fields() {
|
||||
// all fields are required
|
||||
assert!(
|
||||
serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts"])).is_err()
|
||||
);
|
||||
assert!(serde_json::from_value::<Message>(json!([2, 42])).is_err());
|
||||
assert!(serde_json::from_value::<Message>(json!([2])).is_err());
|
||||
assert!(serde_json::from_value::<Message>(json!([])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_command() {
|
||||
assert!(serde_json::from_value::<Message>(json!([0, 42, "hooba", {}])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_error() {
|
||||
assert!(serde_json::from_value::<Message>(json!([1, 42, "flooba", {}])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_id_bounds() {
|
||||
let overflow = i64::from(std::u32::MAX) + 1;
|
||||
let underflow = -1;
|
||||
|
||||
fn get_timeouts(message_id: i64) -> Value {
|
||||
json!([0, message_id, "WebDriver:GetTimeouts", {}])
|
||||
}
|
||||
|
||||
assert!(serde_json::from_value::<Message>(get_timeouts(overflow)).is_err());
|
||||
assert!(serde_json::from_value::<Message>(get_timeouts(underflow)).is_err());
|
||||
}
|
||||
}
|
||||
@ -1,223 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde::de;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::common::{Cookie, Timeouts, WebElement};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NewWindow {
|
||||
handle: String,
|
||||
#[serde(rename = "type")]
|
||||
type_hint: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WindowRect {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ElementRect {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum MarionetteResult {
|
||||
#[serde(deserialize_with = "from_value", serialize_with = "to_value")]
|
||||
Bool(bool),
|
||||
#[serde(deserialize_with = "from_value", serialize_with = "to_empty_value")]
|
||||
Null,
|
||||
NewWindow(NewWindow),
|
||||
WindowRect(WindowRect),
|
||||
ElementRect(ElementRect),
|
||||
#[serde(deserialize_with = "from_value", serialize_with = "to_value")]
|
||||
String(String),
|
||||
Strings(Vec<String>),
|
||||
#[serde(deserialize_with = "from_value", serialize_with = "to_value")]
|
||||
WebElement(WebElement),
|
||||
WebElements(Vec<WebElement>),
|
||||
Cookies(Vec<Cookie>),
|
||||
Timeouts(Timeouts),
|
||||
}
|
||||
|
||||
fn to_value<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: Serialize,
|
||||
{
|
||||
#[derive(Serialize)]
|
||||
struct Wrapper<T> {
|
||||
value: T,
|
||||
}
|
||||
|
||||
Wrapper { value: data }.serialize(serializer)
|
||||
}
|
||||
|
||||
fn to_empty_value<S>(serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
#[derive(Serialize)]
|
||||
struct Wrapper {
|
||||
value: Value,
|
||||
}
|
||||
|
||||
Wrapper { value: Value::Null }.serialize(serializer)
|
||||
}
|
||||
|
||||
fn from_value<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: serde::de::DeserializeOwned,
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Wrapper<T> {
|
||||
value: T,
|
||||
}
|
||||
|
||||
let v = Value::deserialize(deserializer)?;
|
||||
if v.is_object() {
|
||||
let w = serde_json::from_value::<Wrapper<T>>(v).map_err(de::Error::custom)?;
|
||||
Ok(w.value)
|
||||
} else {
|
||||
Err(de::Error::custom("Cannot be deserialized to struct"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::{assert_de, assert_ser_de, ELEMENT_KEY};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_boolean_response() {
|
||||
assert_ser_de(&MarionetteResult::Bool(true), json!({"value": true}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cookies_response() {
|
||||
let mut data = Vec::new();
|
||||
data.push(Cookie {
|
||||
name: "foo".into(),
|
||||
value: "bar".into(),
|
||||
path: Some("/common".into()),
|
||||
domain: Some("web-platform.test".into()),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
expiry: None,
|
||||
same_site: Some("Strict".into()),
|
||||
});
|
||||
assert_ser_de(
|
||||
&MarionetteResult::Cookies(data),
|
||||
json!([{"name":"foo","value":"bar","path":"/common","domain":"web-platform.test","secure":false,"httpOnly":false,"sameSite":"Strict"}]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_window_response() {
|
||||
let data = NewWindow {
|
||||
handle: "6442450945".into(),
|
||||
type_hint: "tab".into(),
|
||||
};
|
||||
let json = json!({"handle": "6442450945", "type": "tab"});
|
||||
assert_ser_de(&MarionetteResult::NewWindow(data), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_web_element_response() {
|
||||
let data = WebElement {
|
||||
element: "foo".into(),
|
||||
};
|
||||
assert_ser_de(
|
||||
&MarionetteResult::WebElement(data),
|
||||
json!({"value": {ELEMENT_KEY: "foo"}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_web_elements_response() {
|
||||
let data = vec![
|
||||
WebElement {
|
||||
element: "foo".into(),
|
||||
},
|
||||
WebElement {
|
||||
element: "bar".into(),
|
||||
},
|
||||
];
|
||||
assert_ser_de(
|
||||
&MarionetteResult::WebElements(data),
|
||||
json!([{ELEMENT_KEY: "foo"}, {ELEMENT_KEY: "bar"}]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeouts_response() {
|
||||
let data = Timeouts {
|
||||
implicit: Some(1000),
|
||||
page_load: Some(200000),
|
||||
script: Some(Some(60000)),
|
||||
};
|
||||
assert_ser_de(
|
||||
&MarionetteResult::Timeouts(data),
|
||||
json!({"implicit":1000,"pageLoad":200000,"script":60000}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_response() {
|
||||
assert_ser_de(
|
||||
&MarionetteResult::String("foo".into()),
|
||||
json!({"value": "foo"}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strings_response() {
|
||||
assert_ser_de(
|
||||
&MarionetteResult::Strings(vec!["2147483649".to_string()]),
|
||||
json!(["2147483649"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_response() {
|
||||
assert_ser_de(&MarionetteResult::Null, json!({ "value": null }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_rect_response() {
|
||||
let data = WindowRect {
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 800,
|
||||
height: 600,
|
||||
};
|
||||
let json = json!({"x": 100, "y": 100, "width": 800, "height": 600});
|
||||
assert_ser_de(&MarionetteResult::WindowRect(data), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_element_rect_response() {
|
||||
let data = ElementRect {
|
||||
x: 8.0,
|
||||
y: 8.0,
|
||||
width: 148.6666717529297,
|
||||
height: 22.0,
|
||||
};
|
||||
let json = json!({"x": 8, "y": 8, "width": 148.6666717529297, "height": 22});
|
||||
assert_de(&MarionetteResult::ElementRect(data), json);
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pub static ELEMENT_KEY: &'static str = "element-6066-11e4-a52e-4f735466cecf";
|
||||
|
||||
pub fn assert_ser_de<T>(data: &T, json: serde_json::Value)
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
T: std::cmp::PartialEq,
|
||||
T: serde::de::DeserializeOwned,
|
||||
T: serde::Serialize,
|
||||
{
|
||||
assert_eq!(serde_json::to_value(data).unwrap(), json);
|
||||
assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn assert_ser<T>(data: &T, json: serde_json::Value)
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
T: std::cmp::PartialEq,
|
||||
T: serde::Serialize,
|
||||
{
|
||||
assert_eq!(serde_json::to_value(data).unwrap(), json);
|
||||
}
|
||||
|
||||
pub fn assert_de<T>(data: &T, json: serde_json::Value)
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
T: std::cmp::PartialEq,
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
|
||||
}
|
||||
@ -1,512 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::common::{from_cookie, from_name, to_cookie, to_name, Cookie, Frame, Timeouts, Window};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Url {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Locator {
|
||||
pub using: Selector,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Selector {
|
||||
#[serde(rename = "css selector")]
|
||||
Css,
|
||||
#[serde(rename = "link text")]
|
||||
LinkText,
|
||||
#[serde(rename = "partial link text")]
|
||||
PartialLinkText,
|
||||
#[serde(rename = "tag name")]
|
||||
TagName,
|
||||
#[serde(rename = "xpath")]
|
||||
XPath,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NewWindow {
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub type_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WindowRect {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub x: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub y: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub width: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub height: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Keys {
|
||||
pub text: String,
|
||||
pub value: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct PrintParameters {
|
||||
pub orientation: PrintOrientation,
|
||||
pub scale: f64,
|
||||
pub background: bool,
|
||||
pub page: PrintPage,
|
||||
pub margin: PrintMargins,
|
||||
pub page_ranges: Vec<String>,
|
||||
pub shrink_to_fit: bool,
|
||||
}
|
||||
|
||||
impl Default for PrintParameters {
|
||||
fn default() -> Self {
|
||||
PrintParameters {
|
||||
orientation: PrintOrientation::default(),
|
||||
scale: 1.0,
|
||||
background: false,
|
||||
page: PrintPage::default(),
|
||||
margin: PrintMargins::default(),
|
||||
page_ranges: Vec::new(),
|
||||
shrink_to_fit: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PrintOrientation {
|
||||
Landscape,
|
||||
Portrait,
|
||||
}
|
||||
|
||||
impl Default for PrintOrientation {
|
||||
fn default() -> Self {
|
||||
PrintOrientation::Portrait
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PrintPage {
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
impl Default for PrintPage {
|
||||
fn default() -> Self {
|
||||
PrintPage {
|
||||
width: 21.59,
|
||||
height: 27.94,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PrintMargins {
|
||||
pub top: f64,
|
||||
pub bottom: f64,
|
||||
pub left: f64,
|
||||
pub right: f64,
|
||||
}
|
||||
|
||||
impl Default for PrintMargins {
|
||||
fn default() -> Self {
|
||||
PrintMargins {
|
||||
top: 1.0,
|
||||
bottom: 1.0,
|
||||
left: 1.0,
|
||||
right: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ScreenshotOptions {
|
||||
pub id: Option<String>,
|
||||
pub highlights: Vec<Option<String>>,
|
||||
pub full: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Script {
|
||||
pub script: String,
|
||||
pub args: Option<Vec<Value>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Command {
|
||||
#[serde(rename = "WebDriver:AcceptAlert")]
|
||||
AcceptAlert,
|
||||
#[serde(
|
||||
rename = "WebDriver:AddCookie",
|
||||
serialize_with = "to_cookie",
|
||||
deserialize_with = "from_cookie"
|
||||
)]
|
||||
AddCookie(Cookie),
|
||||
#[serde(rename = "WebDriver:CloseWindow")]
|
||||
CloseWindow,
|
||||
#[serde(
|
||||
rename = "WebDriver:DeleteCookie",
|
||||
serialize_with = "to_name",
|
||||
deserialize_with = "from_name"
|
||||
)]
|
||||
DeleteCookie(String),
|
||||
#[serde(rename = "WebDriver:DeleteAllCookies")]
|
||||
DeleteCookies,
|
||||
#[serde(rename = "WebDriver:DeleteSession")]
|
||||
DeleteSession,
|
||||
#[serde(rename = "WebDriver:DismissAlert")]
|
||||
DismissAlert,
|
||||
#[serde(rename = "WebDriver:ElementClear")]
|
||||
ElementClear { id: String },
|
||||
#[serde(rename = "WebDriver:ElementClick")]
|
||||
ElementClick { id: String },
|
||||
#[serde(rename = "WebDriver:ElementSendKeys")]
|
||||
ElementSendKeys {
|
||||
id: String,
|
||||
text: String,
|
||||
value: Vec<String>,
|
||||
},
|
||||
#[serde(rename = "WebDriver:ExecuteAsyncScript")]
|
||||
ExecuteAsyncScript(Script),
|
||||
#[serde(rename = "WebDriver:ExecuteScript")]
|
||||
ExecuteScript(Script),
|
||||
#[serde(rename = "WebDriver:FindElement")]
|
||||
FindElement(Locator),
|
||||
#[serde(rename = "WebDriver:FindElements")]
|
||||
FindElements(Locator),
|
||||
#[serde(rename = "WebDriver:FindElement")]
|
||||
FindElementElement {
|
||||
element: String,
|
||||
using: Selector,
|
||||
value: String,
|
||||
},
|
||||
#[serde(rename = "WebDriver:FindElements")]
|
||||
FindElementElements {
|
||||
element: String,
|
||||
using: Selector,
|
||||
value: String,
|
||||
},
|
||||
#[serde(rename = "WebDriver:FindElementFromShadowRoot")]
|
||||
FindShadowRootElement {
|
||||
#[serde(rename = "shadowRoot")]
|
||||
shadow_root: String,
|
||||
using: Selector,
|
||||
value: String,
|
||||
},
|
||||
#[serde(rename = "WebDriver:FindElementsFromShadowRoot")]
|
||||
FindShadowRootElements {
|
||||
#[serde(rename = "shadowRoot")]
|
||||
shadow_root: String,
|
||||
using: Selector,
|
||||
value: String,
|
||||
},
|
||||
#[serde(rename = "WebDriver:FullscreenWindow")]
|
||||
FullscreenWindow,
|
||||
#[serde(rename = "WebDriver:Navigate")]
|
||||
Get(Url),
|
||||
#[serde(rename = "WebDriver:GetActiveElement")]
|
||||
GetActiveElement,
|
||||
#[serde(rename = "WebDriver:GetAlertText")]
|
||||
GetAlertText,
|
||||
#[serde(rename = "WebDriver:GetComputedLabel")]
|
||||
GetComputedLabel { id: String },
|
||||
#[serde(rename = "WebDriver:GetComputedRole")]
|
||||
GetComputedRole { id: String },
|
||||
#[serde(rename = "WebDriver:GetCookies")]
|
||||
GetCookies,
|
||||
#[serde(rename = "WebDriver:GetElementCSSValue")]
|
||||
GetCSSValue {
|
||||
id: String,
|
||||
#[serde(rename = "propertyName")]
|
||||
property: String,
|
||||
},
|
||||
#[serde(rename = "WebDriver:GetCurrentURL")]
|
||||
GetCurrentUrl,
|
||||
#[serde(rename = "WebDriver:GetElementAttribute")]
|
||||
GetElementAttribute { id: String, name: String },
|
||||
#[serde(rename = "WebDriver:GetElementProperty")]
|
||||
GetElementProperty { id: String, name: String },
|
||||
#[serde(rename = "WebDriver:GetElementRect")]
|
||||
GetElementRect { id: String },
|
||||
#[serde(rename = "WebDriver:GetElementTagName")]
|
||||
GetElementTagName { id: String },
|
||||
#[serde(rename = "WebDriver:GetElementText")]
|
||||
GetElementText { id: String },
|
||||
#[serde(rename = "WebDriver:GetPageSource")]
|
||||
GetPageSource,
|
||||
#[serde(rename = "WebDriver:GetShadowRoot")]
|
||||
GetShadowRoot { id: String },
|
||||
#[serde(rename = "WebDriver:GetTimeouts")]
|
||||
GetTimeouts,
|
||||
#[serde(rename = "WebDriver:GetTitle")]
|
||||
GetTitle,
|
||||
#[serde(rename = "WebDriver:GetWindowHandle")]
|
||||
GetWindowHandle,
|
||||
#[serde(rename = "WebDriver:GetWindowHandles")]
|
||||
GetWindowHandles,
|
||||
#[serde(rename = "WebDriver:GetWindowRect")]
|
||||
GetWindowRect,
|
||||
#[serde(rename = "WebDriver:Back")]
|
||||
GoBack,
|
||||
#[serde(rename = "WebDriver:Forward")]
|
||||
GoForward,
|
||||
#[serde(rename = "WebDriver:IsElementDisplayed")]
|
||||
IsDisplayed { id: String },
|
||||
#[serde(rename = "WebDriver:IsElementEnabled")]
|
||||
IsEnabled { id: String },
|
||||
#[serde(rename = "WebDriver:IsElementSelected")]
|
||||
IsSelected { id: String },
|
||||
#[serde(rename = "WebDriver:MaximizeWindow")]
|
||||
MaximizeWindow,
|
||||
#[serde(rename = "WebDriver:MinimizeWindow")]
|
||||
MinimizeWindow,
|
||||
#[serde(rename = "WebDriver:NewWindow")]
|
||||
NewWindow(NewWindow),
|
||||
#[serde(rename = "WebDriver:Print")]
|
||||
Print(PrintParameters),
|
||||
#[serde(rename = "WebDriver:Refresh")]
|
||||
Refresh,
|
||||
#[serde(rename = "WebDriver:ReleaseActions")]
|
||||
ReleaseActions,
|
||||
#[serde(rename = "WebDriver:SendAlertText")]
|
||||
SendAlertText(Keys),
|
||||
#[serde(rename = "WebDriver:SetTimeouts")]
|
||||
SetTimeouts(Timeouts),
|
||||
#[serde(rename = "WebDriver:SetWindowRect")]
|
||||
SetWindowRect(WindowRect),
|
||||
#[serde(rename = "WebDriver:SwitchToFrame")]
|
||||
SwitchToFrame(Frame),
|
||||
#[serde(rename = "WebDriver:SwitchToParentFrame")]
|
||||
SwitchToParentFrame,
|
||||
#[serde(rename = "WebDriver:SwitchToWindow")]
|
||||
SwitchToWindow(Window),
|
||||
#[serde(rename = "WebDriver:TakeScreenshot")]
|
||||
TakeElementScreenshot(ScreenshotOptions),
|
||||
#[serde(rename = "WebDriver:TakeScreenshot")]
|
||||
TakeFullScreenshot(ScreenshotOptions),
|
||||
#[serde(rename = "WebDriver:TakeScreenshot")]
|
||||
TakeScreenshot(ScreenshotOptions),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::Date;
|
||||
use crate::test::{assert_ser, assert_ser_de};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_json_screenshot() {
|
||||
let data = ScreenshotOptions {
|
||||
id: None,
|
||||
highlights: vec![],
|
||||
full: false,
|
||||
};
|
||||
let json = json!({"full":false,"highlights":[],"id":null});
|
||||
assert_ser_de(&data, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_selector_css() {
|
||||
assert_ser_de(&Selector::Css, json!("css selector"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_selector_link_text() {
|
||||
assert_ser_de(&Selector::LinkText, json!("link text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_selector_partial_link_text() {
|
||||
assert_ser_de(&Selector::PartialLinkText, json!("partial link text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_selector_tag_name() {
|
||||
assert_ser_de(&Selector::TagName, json!("tag name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_selector_xpath() {
|
||||
assert_ser_de(&Selector::XPath, json!("xpath"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_selector_invalid() {
|
||||
assert!(serde_json::from_value::<Selector>(json!("foo")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_locator() {
|
||||
let json = json!({
|
||||
"using": "partial link text",
|
||||
"value": "link text",
|
||||
});
|
||||
let data = Locator {
|
||||
using: Selector::PartialLinkText,
|
||||
value: "link text".into(),
|
||||
};
|
||||
|
||||
assert_ser_de(&data, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_keys() {
|
||||
let data = Keys {
|
||||
text: "Foo".into(),
|
||||
value: vec!["F".into(), "o".into(), "o".into()],
|
||||
};
|
||||
let json = json!({"text": "Foo", "value": ["F", "o", "o"]});
|
||||
assert_ser_de(&data, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_new_window() {
|
||||
let data = NewWindow {
|
||||
type_hint: Some("foo".into()),
|
||||
};
|
||||
assert_ser_de(&data, json!({ "type": "foo" }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_window_rect() {
|
||||
let data = WindowRect {
|
||||
x: Some(123),
|
||||
y: None,
|
||||
width: None,
|
||||
height: None,
|
||||
};
|
||||
assert_ser_de(&data, json!({"x": 123}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_with_params() {
|
||||
let locator = Locator {
|
||||
using: Selector::Css,
|
||||
value: "value".into(),
|
||||
};
|
||||
let json = json!({"WebDriver:FindElement": {"using": "css selector", "value": "value"}});
|
||||
assert_ser_de(&Command::FindElement(locator), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_with_wrapper_params() {
|
||||
let cookie = Cookie {
|
||||
name: "hello".into(),
|
||||
value: "world".into(),
|
||||
path: None,
|
||||
domain: None,
|
||||
secure: false,
|
||||
http_only: false,
|
||||
expiry: Some(Date(1564488092)),
|
||||
same_site: None,
|
||||
};
|
||||
let json = json!({"WebDriver:AddCookie": {"cookie": {"name": "hello", "value": "world", "secure": false, "httpOnly": false, "expiry": 1564488092}}});
|
||||
assert_ser_de(&Command::AddCookie(cookie), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_commands() {
|
||||
assert_ser_de(&Command::GetTimeouts, json!("WebDriver:GetTimeouts"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_command_invalid() {
|
||||
assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_delete_cookie_command() {
|
||||
let json = json!({"WebDriver:DeleteCookie": {"name": "foo"}});
|
||||
assert_ser_de(&Command::DeleteCookie("foo".into()), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_new_window_command() {
|
||||
let data = NewWindow {
|
||||
type_hint: Some("foo".into()),
|
||||
};
|
||||
let json = json!({"WebDriver:NewWindow": {"type": "foo"}});
|
||||
assert_ser_de(&Command::NewWindow(data), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_new_window_command_with_none_value() {
|
||||
let data = NewWindow { type_hint: None };
|
||||
let json = json!({"WebDriver:NewWindow": {}});
|
||||
assert_ser_de(&Command::NewWindow(data), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_command_as_struct() {
|
||||
assert_ser(
|
||||
&Command::FindElementElement {
|
||||
element: "foo".into(),
|
||||
using: Selector::XPath,
|
||||
value: "bar".into(),
|
||||
},
|
||||
json!({"WebDriver:FindElement": {"element": "foo", "using": "xpath", "value": "bar" }}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_get_computed_label_command() {
|
||||
assert_ser_de(
|
||||
&Command::GetComputedLabel { id: "foo".into() },
|
||||
json!({"WebDriver:GetComputedLabel": {"id": "foo"}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_get_computed_role_command() {
|
||||
assert_ser_de(
|
||||
&Command::GetComputedRole { id: "foo".into() },
|
||||
json!({"WebDriver:GetComputedRole": {"id": "foo"}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_get_css_value() {
|
||||
assert_ser_de(
|
||||
&Command::GetCSSValue {
|
||||
id: "foo".into(),
|
||||
property: "bar".into(),
|
||||
},
|
||||
json!({"WebDriver:GetElementCSSValue": {"id": "foo", "propertyName": "bar"}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_find_shadow_root_element() {
|
||||
assert_ser_de(
|
||||
&Command::FindShadowRootElement {
|
||||
shadow_root: "foo".into(),
|
||||
using: Selector::Css,
|
||||
value: "bar".into(),
|
||||
},
|
||||
json!({"WebDriver:FindElementFromShadowRoot": {"shadowRoot": "foo", "using": "css selector", "value": "bar"}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_find_shadow_root_elements() {
|
||||
assert_ser_de(
|
||||
&Command::FindShadowRootElements {
|
||||
shadow_root: "foo".into(),
|
||||
using: Selector::Css,
|
||||
value: "bar".into(),
|
||||
},
|
||||
json!({"WebDriver:FindElementsFromShadowRoot": {"shadowRoot": "foo", "using": "css selector", "value": "bar"}}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,533 +0,0 @@
|
||||
use crate::capabilities::AndroidOptions;
|
||||
use mozdevice::{AndroidStorage, Device, Host, UnixPathBuf};
|
||||
use mozprofile::profile::Profile;
|
||||
use serde::Serialize;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::time;
|
||||
use webdriver::error::{ErrorStatus, WebDriverError};
|
||||
|
||||
// TODO: avoid port clashes across GeckoView-vehicles.
|
||||
// For now, we always use target port 2829, leading to issues like bug 1533704.
|
||||
const MARIONETTE_TARGET_PORT: u16 = 2829;
|
||||
|
||||
const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML
|
||||
##
|
||||
## Auto-generated by geckodriver.
|
||||
## See https://mozilla.github.io/geckoview/consumer/docs/automation.
|
||||
"#;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AndroidError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AndroidError {
|
||||
ActivityNotFound(String),
|
||||
Device(mozdevice::DeviceError),
|
||||
IO(io::Error),
|
||||
PackageNotFound(String),
|
||||
Serde(serde_yaml::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for AndroidError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
AndroidError::ActivityNotFound(ref package) => {
|
||||
write!(f, "Activity for package '{}' not found", package)
|
||||
}
|
||||
AndroidError::Device(ref message) => message.fmt(f),
|
||||
AndroidError::IO(ref message) => message.fmt(f),
|
||||
AndroidError::PackageNotFound(ref package) => {
|
||||
write!(f, "Package '{}' not found", package)
|
||||
}
|
||||
AndroidError::Serde(ref message) => message.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for AndroidError {
|
||||
fn from(value: io::Error) -> AndroidError {
|
||||
AndroidError::IO(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mozdevice::DeviceError> for AndroidError {
|
||||
fn from(value: mozdevice::DeviceError) -> AndroidError {
|
||||
AndroidError::Device(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_yaml::Error> for AndroidError {
|
||||
fn from(value: serde_yaml::Error) -> AndroidError {
|
||||
AndroidError::Serde(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AndroidError> for WebDriverError {
|
||||
fn from(value: AndroidError) -> WebDriverError {
|
||||
WebDriverError::new(ErrorStatus::UnknownError, value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A remote Gecko instance.
|
||||
///
|
||||
/// Host refers to the device running `geckodriver`. Target refers to the
|
||||
/// Android device running Gecko in a GeckoView-based vehicle.
|
||||
#[derive(Debug)]
|
||||
pub struct AndroidProcess {
|
||||
pub device: Device,
|
||||
pub package: String,
|
||||
pub activity: String,
|
||||
}
|
||||
|
||||
impl AndroidProcess {
|
||||
pub fn new(
|
||||
device: Device,
|
||||
package: String,
|
||||
activity: String,
|
||||
) -> mozdevice::Result<AndroidProcess> {
|
||||
Ok(AndroidProcess {
|
||||
device,
|
||||
package,
|
||||
activity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AndroidHandler {
|
||||
pub config: UnixPathBuf,
|
||||
pub options: AndroidOptions,
|
||||
pub process: AndroidProcess,
|
||||
pub profile: UnixPathBuf,
|
||||
pub test_root: UnixPathBuf,
|
||||
|
||||
// Port forwarding for Marionette: host => target
|
||||
pub marionette_host_port: u16,
|
||||
pub marionette_target_port: u16,
|
||||
|
||||
// Port forwarding for WebSocket connections (WebDriver BiDi and CDP)
|
||||
pub websocket_port: Option<u16>,
|
||||
}
|
||||
|
||||
impl Drop for AndroidHandler {
|
||||
fn drop(&mut self) {
|
||||
// Try to clean up various settings
|
||||
let clear_command = format!("am clear-debug-app {}", self.process.package);
|
||||
match self
|
||||
.process
|
||||
.device
|
||||
.execute_host_shell_command(&clear_command)
|
||||
{
|
||||
Ok(_) => debug!("Disabled reading from configuration file"),
|
||||
Err(e) => error!("Failed disabling from configuration file: {}", e),
|
||||
}
|
||||
|
||||
match self.process.device.remove(&self.config) {
|
||||
Ok(_) => debug!("Deleted GeckoView configuration file"),
|
||||
Err(e) => error!("Failed deleting GeckoView configuration file: {}", e),
|
||||
}
|
||||
|
||||
match self.process.device.remove(&self.test_root) {
|
||||
Ok(_) => debug!("Deleted test root folder: {}", &self.test_root.display()),
|
||||
Err(e) => error!("Failed deleting test root folder: {}", e),
|
||||
}
|
||||
|
||||
match self
|
||||
.process
|
||||
.device
|
||||
.kill_forward_port(self.marionette_host_port)
|
||||
{
|
||||
Ok(_) => debug!(
|
||||
"Marionette port forward ({} -> {}) stopped",
|
||||
&self.marionette_host_port, &self.marionette_target_port
|
||||
),
|
||||
Err(e) => error!(
|
||||
"Marionette port forward ({} -> {}) failed to stop: {}",
|
||||
&self.marionette_host_port, &self.marionette_target_port, e
|
||||
),
|
||||
}
|
||||
|
||||
if let Some(port) = self.websocket_port {
|
||||
match self.process.device.kill_forward_port(port) {
|
||||
Ok(_) => debug!("WebSocket port forward ({0} -> {0}) stopped", &port),
|
||||
Err(e) => error!(
|
||||
"WebSocket port forward ({0} -> {0}) failed to stop: {1}",
|
||||
&port, e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AndroidHandler {
|
||||
pub fn new(
|
||||
options: &AndroidOptions,
|
||||
marionette_host_port: u16,
|
||||
websocket_port: Option<u16>,
|
||||
) -> Result<AndroidHandler> {
|
||||
// We need to push profile.pathbuf to a safe space on the device.
|
||||
// Make it per-Android package to avoid clashes and confusion.
|
||||
// This naming scheme follows GeckoView's configuration file naming scheme,
|
||||
// see bug 1533385.
|
||||
|
||||
let host = Host {
|
||||
host: None,
|
||||
port: None,
|
||||
read_timeout: Some(time::Duration::from_millis(5000)),
|
||||
write_timeout: Some(time::Duration::from_millis(5000)),
|
||||
};
|
||||
|
||||
let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?;
|
||||
|
||||
// Set up port forwarding for Marionette.
|
||||
device.forward_port(marionette_host_port, MARIONETTE_TARGET_PORT)?;
|
||||
debug!(
|
||||
"Marionette port forward ({} -> {}) started",
|
||||
marionette_host_port, MARIONETTE_TARGET_PORT
|
||||
);
|
||||
|
||||
if let Some(port) = websocket_port {
|
||||
// Set up port forwarding for WebSocket connections (WebDriver BiDi, and CDP).
|
||||
device.forward_port(port, port)?;
|
||||
debug!("WebSocket port forward ({} -> {}) started", port, port);
|
||||
}
|
||||
|
||||
let test_root = match device.storage {
|
||||
AndroidStorage::App => {
|
||||
device.run_as_package = Some(options.package.to_owned());
|
||||
let mut buf = UnixPathBuf::from("/data/data");
|
||||
buf.push(&options.package);
|
||||
buf.push("test_root");
|
||||
buf
|
||||
}
|
||||
AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
|
||||
AndroidStorage::Sdcard => {
|
||||
// We need to push the profile to a location on the device that can also
|
||||
// be read and write by the application, and works for unrooted devices.
|
||||
// The only location that meets this criteria is under:
|
||||
// $EXTERNAL_STORAGE/Android/data/%options.package%/files
|
||||
let response = device.execute_host_shell_command("echo $EXTERNAL_STORAGE")?;
|
||||
let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
|
||||
buf.push("Android/data");
|
||||
buf.push(&options.package);
|
||||
buf.push("files/test_root");
|
||||
buf
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Connecting: options={:?}, storage={:?}) test_root={}, run_as_package={:?}",
|
||||
options,
|
||||
device.storage,
|
||||
test_root.display(),
|
||||
device.run_as_package
|
||||
);
|
||||
|
||||
let mut profile = test_root.clone();
|
||||
profile.push(format!("{}-geckodriver-profile", &options.package));
|
||||
|
||||
// Check if the specified package is installed
|
||||
let response =
|
||||
device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?;
|
||||
let mut packages = response
|
||||
.trim()
|
||||
.split_terminator('\n')
|
||||
.filter(|line| line.starts_with("package:"))
|
||||
.map(|line| line.rsplit(':').next().expect("Package name found"));
|
||||
if !packages.any(|x| x == options.package.as_str()) {
|
||||
return Err(AndroidError::PackageNotFound(options.package.clone()));
|
||||
}
|
||||
|
||||
let config = UnixPathBuf::from(format!(
|
||||
"/data/local/tmp/{}-geckoview-config.yaml",
|
||||
&options.package
|
||||
));
|
||||
|
||||
// If activity hasn't been specified default to the main activity of the package
|
||||
let activity = match options.activity {
|
||||
Some(ref activity) => activity.clone(),
|
||||
None => {
|
||||
let response = device.execute_host_shell_command(&format!(
|
||||
"cmd package resolve-activity --brief {}",
|
||||
&options.package
|
||||
))?;
|
||||
let activities = response
|
||||
.split_terminator('\n')
|
||||
.filter(|line| line.starts_with(&options.package))
|
||||
.map(|line| line.rsplit('/').next().unwrap())
|
||||
.collect::<Vec<&str>>();
|
||||
if activities.is_empty() {
|
||||
return Err(AndroidError::ActivityNotFound(options.package.clone()));
|
||||
}
|
||||
|
||||
activities[0].to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
let process = AndroidProcess::new(device, options.package.clone(), activity)?;
|
||||
|
||||
Ok(AndroidHandler {
|
||||
config,
|
||||
process,
|
||||
profile,
|
||||
test_root,
|
||||
marionette_host_port,
|
||||
marionette_target_port: MARIONETTE_TARGET_PORT,
|
||||
options: options.clone(),
|
||||
websocket_port,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_config_file<I, K, V>(
|
||||
&self,
|
||||
args: Option<Vec<String>>,
|
||||
envs: I,
|
||||
) -> Result<String>
|
||||
where
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
K: ToString,
|
||||
V: ToString,
|
||||
{
|
||||
// To configure GeckoView, we use the automation techniques documented at
|
||||
// https://mozilla.github.io/geckoview/consumer/docs/automation.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
|
||||
pub struct Config {
|
||||
pub env: Mapping,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
let mut config = Config {
|
||||
args: vec![
|
||||
"--marionette".into(),
|
||||
"--profile".into(),
|
||||
self.profile.display().to_string(),
|
||||
],
|
||||
env: Mapping::new(),
|
||||
};
|
||||
|
||||
config.args.append(&mut args.unwrap_or_default());
|
||||
|
||||
for (key, value) in envs {
|
||||
config.env.insert(
|
||||
Value::String(key.to_string()),
|
||||
Value::String(value.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
config.env.insert(
|
||||
Value::String("MOZ_CRASHREPORTER".to_owned()),
|
||||
Value::String("1".to_owned()),
|
||||
);
|
||||
config.env.insert(
|
||||
Value::String("MOZ_CRASHREPORTER_NO_REPORT".to_owned()),
|
||||
Value::String("1".to_owned()),
|
||||
);
|
||||
config.env.insert(
|
||||
Value::String("MOZ_CRASHREPORTER_SHUTDOWN".to_owned()),
|
||||
Value::String("1".to_owned()),
|
||||
);
|
||||
|
||||
let mut contents: Vec<String> = vec![CONFIG_FILE_HEADING.to_owned()];
|
||||
contents.push(serde_yaml::to_string(&config)?);
|
||||
|
||||
Ok(contents.concat())
|
||||
}
|
||||
|
||||
pub fn prepare<I, K, V>(
|
||||
&self,
|
||||
profile: &Profile,
|
||||
args: Option<Vec<String>>,
|
||||
env: I,
|
||||
) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
K: ToString,
|
||||
V: ToString,
|
||||
{
|
||||
self.process.device.clear_app_data(&self.process.package)?;
|
||||
|
||||
// These permissions, at least, are required to read profiles in /mnt/sdcard.
|
||||
for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] {
|
||||
self.process.device.execute_host_shell_command(&format!(
|
||||
"pm grant {} android.permission.{}",
|
||||
&self.process.package, perm
|
||||
))?;
|
||||
}
|
||||
|
||||
// Make sure to create the test root.
|
||||
self.process.device.create_dir(&self.test_root)?;
|
||||
self.process.device.chmod(&self.test_root, "777", true)?;
|
||||
|
||||
// Replace the profile
|
||||
self.process.device.remove(&self.profile)?;
|
||||
self.process
|
||||
.device
|
||||
.push_dir(&profile.path, &self.profile, 0o777)?;
|
||||
|
||||
let contents = self.generate_config_file(args, env)?;
|
||||
debug!("Content of generated GeckoView config file:\n{}", contents);
|
||||
let reader = &mut io::BufReader::new(contents.as_bytes());
|
||||
|
||||
debug!(
|
||||
"Pushing GeckoView configuration file to {}",
|
||||
self.config.display()
|
||||
);
|
||||
self.process.device.push(reader, &self.config, 0o777)?;
|
||||
|
||||
// Tell GeckoView to read configuration even when `android:debuggable="false"`.
|
||||
self.process.device.execute_host_shell_command(&format!(
|
||||
"am set-debug-app --persistent {}",
|
||||
self.process.package
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> Result<()> {
|
||||
// TODO: Remove the usage of intent arguments once Fennec is no longer
|
||||
// supported. Packages which are using GeckoView always read the arguments
|
||||
// via the YAML configuration file.
|
||||
let mut intent_arguments = self
|
||||
.options
|
||||
.intent_arguments
|
||||
.clone()
|
||||
.unwrap_or_else(|| Vec::with_capacity(3));
|
||||
intent_arguments.push("--es".to_owned());
|
||||
intent_arguments.push("args".to_owned());
|
||||
intent_arguments.push(format!("--marionette --profile {}", self.profile.display()));
|
||||
|
||||
debug!(
|
||||
"Launching {}/{}",
|
||||
self.process.package, self.process.activity
|
||||
);
|
||||
self.process
|
||||
.device
|
||||
.launch(
|
||||
&self.process.package,
|
||||
&self.process.activity,
|
||||
&intent_arguments,
|
||||
)
|
||||
.map_err(|e| {
|
||||
let message = format!(
|
||||
"Could not launch Android {}/{}: {}",
|
||||
self.process.package, self.process.activity, e
|
||||
);
|
||||
mozdevice::DeviceError::Adb(message)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn force_stop(&self) -> Result<()> {
|
||||
debug!(
|
||||
"Force stopping the Android package: {}",
|
||||
&self.process.package
|
||||
);
|
||||
self.process.device.force_stop(&self.process.package)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// To successfully run those tests the geckoview_example package needs to
|
||||
// be installed on the device or emulator. After setting up the build
|
||||
// environment (https://mzl.la/3muLv5M), the following mach commands have to
|
||||
// be executed:
|
||||
//
|
||||
// $ ./mach build && ./mach install
|
||||
//
|
||||
// Currently the mozdevice API is not safe for multiple requests at the same
|
||||
// time. It is recommended to run each of the unit tests on its own. Also adb
|
||||
// specific tests cannot be run in CI yet. To check those locally, also run
|
||||
// the ignored tests.
|
||||
//
|
||||
// Use the following command to accomplish that:
|
||||
//
|
||||
// $ cargo test -- --ignored --test-threads=1
|
||||
|
||||
use crate::android::AndroidHandler;
|
||||
use crate::capabilities::AndroidOptions;
|
||||
use mozdevice::{AndroidStorage, AndroidStorageInput, UnixPathBuf};
|
||||
|
||||
fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) {
|
||||
let options = AndroidOptions::new(package.to_owned(), storage);
|
||||
let handler = AndroidHandler::new(&options, 4242, None).expect("has valid Android handler");
|
||||
|
||||
assert_eq!(handler.options, options);
|
||||
assert_eq!(handler.process.package, package);
|
||||
|
||||
let expected_config_path = UnixPathBuf::from(format!(
|
||||
"/data/local/tmp/{}-geckoview-config.yaml",
|
||||
&package
|
||||
));
|
||||
assert_eq!(handler.config, expected_config_path);
|
||||
|
||||
if handler.process.device.storage == AndroidStorage::App {
|
||||
assert_eq!(
|
||||
handler.process.device.run_as_package,
|
||||
Some(package.to_owned())
|
||||
);
|
||||
} else {
|
||||
assert_eq!(handler.process.device.run_as_package, None);
|
||||
}
|
||||
|
||||
let test_root = match handler.process.device.storage {
|
||||
AndroidStorage::App => {
|
||||
let mut buf = UnixPathBuf::from("/data/data");
|
||||
buf.push(&package);
|
||||
buf.push("test_root");
|
||||
buf
|
||||
}
|
||||
AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
|
||||
AndroidStorage::Sdcard => {
|
||||
let response = handler
|
||||
.process
|
||||
.device
|
||||
.execute_host_shell_command("echo $EXTERNAL_STORAGE")
|
||||
.unwrap();
|
||||
|
||||
let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
|
||||
buf.push("Android/data/");
|
||||
buf.push(&package);
|
||||
buf.push("files/test_root");
|
||||
buf
|
||||
}
|
||||
};
|
||||
assert_eq!(handler.test_root, test_root);
|
||||
|
||||
let mut profile = test_root;
|
||||
profile.push(format!("{}-geckodriver-profile", &package));
|
||||
assert_eq!(handler.profile, profile);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn android_handler_storage_as_app() {
|
||||
let package = "org.mozilla.geckoview_example";
|
||||
run_handler_storage_test(package, AndroidStorageInput::App);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn android_handler_storage_as_auto() {
|
||||
let package = "org.mozilla.geckoview_example";
|
||||
run_handler_storage_test(package, AndroidStorageInput::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn android_handler_storage_as_internal() {
|
||||
let package = "org.mozilla.geckoview_example";
|
||||
run_handler_storage_test(package, AndroidStorageInput::Internal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn android_handler_storage_as_sdcard() {
|
||||
let package = "org.mozilla.geckoview_example";
|
||||
run_handler_storage_test(package, AndroidStorageInput::Sdcard);
|
||||
}
|
||||
}
|
||||
@ -1,554 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::android::AndroidHandler;
|
||||
use crate::capabilities::{FirefoxOptions, ProfileType};
|
||||
use crate::logging;
|
||||
use crate::prefs;
|
||||
use mozprofile::preferences::Pref;
|
||||
use mozprofile::profile::{PrefFile, Profile};
|
||||
use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess};
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time;
|
||||
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
|
||||
|
||||
/// A running Gecko instance.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum Browser {
|
||||
Local(LocalBrowser),
|
||||
Remote(RemoteBrowser),
|
||||
|
||||
/// An existing browser instance not controlled by GeckoDriver
|
||||
Existing(u16),
|
||||
}
|
||||
|
||||
impl Browser {
|
||||
pub(crate) fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> {
|
||||
match self {
|
||||
Browser::Local(x) => x.close(wait_for_shutdown),
|
||||
Browser::Remote(x) => x.close(),
|
||||
Browser::Existing(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
|
||||
match self {
|
||||
Browser::Local(x) => x.marionette_port(),
|
||||
Browser::Remote(x) => x.marionette_port(),
|
||||
Browser::Existing(x) => Ok(Some(*x)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_marionette_port(&mut self, port: u16) {
|
||||
match self {
|
||||
Browser::Local(x) => x.update_marionette_port(port),
|
||||
Browser::Remote(x) => x.update_marionette_port(port),
|
||||
Browser::Existing(x) => {
|
||||
if port != *x {
|
||||
error!(
|
||||
"Cannot re-assign Marionette port when connected to an existing browser"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A local Firefox process, running on this (host) device.
|
||||
pub(crate) struct LocalBrowser {
|
||||
marionette_port: u16,
|
||||
prefs_backup: Option<PrefsBackup>,
|
||||
process: FirefoxProcess,
|
||||
profile_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl LocalBrowser {
|
||||
pub(crate) fn new(
|
||||
options: FirefoxOptions,
|
||||
marionette_port: u16,
|
||||
jsdebugger: bool,
|
||||
profile_root: Option<&Path>,
|
||||
) -> WebDriverResult<LocalBrowser> {
|
||||
let binary = options.binary.ok_or_else(|| {
|
||||
WebDriverError::new(
|
||||
ErrorStatus::SessionNotCreated,
|
||||
"Expected browser binary location, but unable to find \
|
||||
binary in default location, no \
|
||||
'moz:firefoxOptions.binary' capability provided, and \
|
||||
no binary flag set on the command line",
|
||||
)
|
||||
})?;
|
||||
|
||||
let is_custom_profile = matches!(options.profile, ProfileType::Path(_));
|
||||
|
||||
let mut profile = match options.profile {
|
||||
ProfileType::Named => None,
|
||||
ProfileType::Path(x) => Some(x),
|
||||
ProfileType::Temporary => Some(Profile::new(profile_root)?),
|
||||
};
|
||||
|
||||
let (profile_path, prefs_backup) = if let Some(ref mut profile) = profile {
|
||||
let profile_path = profile.path.clone();
|
||||
let prefs_backup = set_prefs(
|
||||
marionette_port,
|
||||
profile,
|
||||
is_custom_profile,
|
||||
options.prefs,
|
||||
jsdebugger,
|
||||
)
|
||||
.map_err(|e| {
|
||||
WebDriverError::new(
|
||||
ErrorStatus::SessionNotCreated,
|
||||
format!("Failed to set preferences: {}", e),
|
||||
)
|
||||
})?;
|
||||
(Some(profile_path), prefs_backup)
|
||||
} else {
|
||||
warn!("Unable to set geckodriver prefs when using a named profile");
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let mut runner = FirefoxRunner::new(&binary, profile);
|
||||
|
||||
runner.arg("--marionette");
|
||||
if jsdebugger {
|
||||
runner.arg("--jsdebugger");
|
||||
}
|
||||
if let Some(args) = options.args.as_ref() {
|
||||
runner.args(args);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
|
||||
runner
|
||||
.env("MOZ_CRASHREPORTER", "1")
|
||||
.env("MOZ_CRASHREPORTER_NO_REPORT", "1")
|
||||
.env("MOZ_CRASHREPORTER_SHUTDOWN", "1");
|
||||
|
||||
let process = match runner.start() {
|
||||
Ok(process) => process,
|
||||
Err(e) => {
|
||||
if let Some(backup) = prefs_backup {
|
||||
backup.restore();
|
||||
}
|
||||
return Err(WebDriverError::new(
|
||||
ErrorStatus::SessionNotCreated,
|
||||
format!("Failed to start browser {}: {}", binary.display(), e),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(LocalBrowser {
|
||||
marionette_port,
|
||||
prefs_backup,
|
||||
process,
|
||||
profile_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn close(mut self, wait_for_shutdown: bool) -> WebDriverResult<()> {
|
||||
if wait_for_shutdown {
|
||||
// TODO(https://bugzil.la/1443922):
|
||||
// Use toolkit.asyncshutdown.crash_timout pref
|
||||
let duration = time::Duration::from_secs(70);
|
||||
match self.process.wait(duration) {
|
||||
Ok(x) => debug!("Browser process stopped: {}", x),
|
||||
Err(e) => error!("Failed to stop browser process: {}", e),
|
||||
}
|
||||
}
|
||||
self.process.kill()?;
|
||||
|
||||
// Restoring the prefs if the browser fails to stop perhaps doesn't work anyway
|
||||
if let Some(prefs_backup) = self.prefs_backup {
|
||||
prefs_backup.restore();
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
|
||||
if self.marionette_port != 0 {
|
||||
return Ok(Some(self.marionette_port));
|
||||
}
|
||||
|
||||
if let Some(profile_path) = self.profile_path.as_ref() {
|
||||
return Ok(read_marionette_port(profile_path));
|
||||
}
|
||||
|
||||
// This should be impossible, but it isn't enforced
|
||||
Err(WebDriverError::new(
|
||||
ErrorStatus::SessionNotCreated,
|
||||
"Port not known when using named profile",
|
||||
))
|
||||
}
|
||||
|
||||
fn update_marionette_port(&mut self, port: u16) {
|
||||
self.marionette_port = port;
|
||||
}
|
||||
|
||||
pub(crate) fn check_status(&mut self) -> Option<String> {
|
||||
match self.process.try_wait() {
|
||||
Ok(Some(status)) => Some(
|
||||
status
|
||||
.code()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "signal".into()),
|
||||
),
|
||||
Ok(None) => None,
|
||||
Err(_) => Some("{unknown}".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_marionette_port(profile_path: &Path) -> Option<u16> {
|
||||
let port_file = profile_path.join("MarionetteActivePort");
|
||||
let mut port_str = String::with_capacity(6);
|
||||
let mut file = match fs::File::open(&port_file) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
trace!("Failed to open {}", &port_file.to_string_lossy());
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Err(e) = file.read_to_string(&mut port_str) {
|
||||
trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e);
|
||||
return None;
|
||||
};
|
||||
println!("Read port: {}", port_str);
|
||||
let port = port_str.parse::<u16>().ok();
|
||||
if port.is_none() {
|
||||
warn!("Failed fo convert {} to u16", &port_str);
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A remote instance, running on a (target) Android device.
|
||||
pub(crate) struct RemoteBrowser {
|
||||
handler: AndroidHandler,
|
||||
marionette_port: u16,
|
||||
prefs_backup: Option<PrefsBackup>,
|
||||
}
|
||||
|
||||
impl RemoteBrowser {
|
||||
pub(crate) fn new(
|
||||
options: FirefoxOptions,
|
||||
marionette_port: u16,
|
||||
websocket_port: Option<u16>,
|
||||
profile_root: Option<&Path>,
|
||||
) -> WebDriverResult<RemoteBrowser> {
|
||||
let android_options = options.android.unwrap();
|
||||
|
||||
let handler = AndroidHandler::new(&android_options, marionette_port, websocket_port)?;
|
||||
|
||||
// Profile management.
|
||||
let (mut profile, is_custom_profile) = match options.profile {
|
||||
ProfileType::Named => {
|
||||
return Err(WebDriverError::new(
|
||||
ErrorStatus::SessionNotCreated,
|
||||
"Cannot use a named profile on Android",
|
||||
));
|
||||
}
|
||||
ProfileType::Path(x) => (x, true),
|
||||
ProfileType::Temporary => (Profile::new(profile_root)?, false),
|
||||
};
|
||||
|
||||
let prefs_backup = set_prefs(
|
||||
handler.marionette_target_port,
|
||||
&mut profile,
|
||||
is_custom_profile,
|
||||
options.prefs,
|
||||
false,
|
||||
)
|
||||
.map_err(|e| {
|
||||
WebDriverError::new(
|
||||
ErrorStatus::SessionNotCreated,
|
||||
format!("Failed to set preferences: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
handler.prepare(&profile, options.args, options.env.unwrap_or_default())?;
|
||||
|
||||
handler.launch()?;
|
||||
|
||||
Ok(RemoteBrowser {
|
||||
handler,
|
||||
marionette_port,
|
||||
prefs_backup,
|
||||
})
|
||||
}
|
||||
|
||||
fn close(self) -> WebDriverResult<()> {
|
||||
self.handler.force_stop()?;
|
||||
|
||||
// Restoring the prefs if the browser fails to stop perhaps doesn't work anyway
|
||||
if let Some(prefs_backup) = self.prefs_backup {
|
||||
prefs_backup.restore();
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
|
||||
Ok(Some(self.marionette_port))
|
||||
}
|
||||
|
||||
fn update_marionette_port(&mut self, port: u16) {
|
||||
self.marionette_port = port;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_prefs(
|
||||
port: u16,
|
||||
profile: &mut Profile,
|
||||
custom_profile: bool,
|
||||
extra_prefs: Vec<(String, Pref)>,
|
||||
js_debugger: bool,
|
||||
) -> WebDriverResult<Option<PrefsBackup>> {
|
||||
let prefs = profile.user_prefs().map_err(|_| {
|
||||
WebDriverError::new(
|
||||
ErrorStatus::UnknownError,
|
||||
"Unable to read profile preferences file",
|
||||
)
|
||||
})?;
|
||||
|
||||
let backup_prefs = if custom_profile && prefs.path.exists() {
|
||||
Some(PrefsBackup::new(prefs)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for &(name, ref value) in prefs::DEFAULT.iter() {
|
||||
if !custom_profile || !prefs.contains_key(name) {
|
||||
prefs.insert(name.to_string(), (*value).clone());
|
||||
}
|
||||
}
|
||||
|
||||
prefs.insert_slice(&extra_prefs[..]);
|
||||
|
||||
if js_debugger {
|
||||
prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger"));
|
||||
prefs.insert("devtools.debugger.remote-enabled", Pref::new(true));
|
||||
prefs.insert("devtools.chrome.enabled", Pref::new(true));
|
||||
prefs.insert("devtools.debugger.prompt-connection", Pref::new(false));
|
||||
}
|
||||
|
||||
prefs.insert("marionette.port", Pref::new(port));
|
||||
prefs.insert("remote.log.level", logging::max_level().into());
|
||||
|
||||
prefs.write().map_err(|e| {
|
||||
WebDriverError::new(
|
||||
ErrorStatus::UnknownError,
|
||||
format!("Unable to write Firefox profile: {}", e),
|
||||
)
|
||||
})?;
|
||||
Ok(backup_prefs)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PrefsBackup {
|
||||
orig_path: PathBuf,
|
||||
backup_path: PathBuf,
|
||||
}
|
||||
|
||||
impl PrefsBackup {
|
||||
fn new(prefs: &PrefFile) -> WebDriverResult<PrefsBackup> {
|
||||
let mut prefs_backup_path = prefs.path.clone();
|
||||
let mut counter = 0;
|
||||
while {
|
||||
let ext = if counter > 0 {
|
||||
format!("geckodriver_backup_{}", counter)
|
||||
} else {
|
||||
"geckodriver_backup".to_string()
|
||||
};
|
||||
prefs_backup_path.set_extension(ext);
|
||||
prefs_backup_path.exists()
|
||||
} {
|
||||
counter += 1
|
||||
}
|
||||
debug!("Backing up prefs to {:?}", prefs_backup_path);
|
||||
fs::copy(&prefs.path, &prefs_backup_path)?;
|
||||
|
||||
Ok(PrefsBackup {
|
||||
orig_path: prefs.path.clone(),
|
||||
backup_path: prefs_backup_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn restore(self) {
|
||||
if self.backup_path.exists() {
|
||||
let _ = fs::rename(self.backup_path, self.orig_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::set_prefs;
|
||||
use crate::browser::read_marionette_port;
|
||||
use crate::capabilities::{FirefoxOptions, ProfileType};
|
||||
use mozprofile::preferences::{Pref, PrefValue};
|
||||
use mozprofile::profile::Profile;
|
||||
use serde_json::{Map, Value};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn example_profile() -> Value {
|
||||
let mut profile_data = Vec::with_capacity(1024);
|
||||
let mut profile = File::open("src/tests/profile.zip").unwrap();
|
||||
profile.read_to_end(&mut profile_data).unwrap();
|
||||
Value::String(base64::encode(&profile_data))
|
||||
}
|
||||
|
||||
// This is not a pretty test, mostly due to the nature of
|
||||
// mozprofile's and MarionetteHandler's APIs, but we have had
|
||||
// several regressions related to remote.log.level.
|
||||
#[test]
|
||||
fn test_remote_log_level() {
|
||||
let mut profile = Profile::new(None).unwrap();
|
||||
set_prefs(2828, &mut profile, false, vec![], false).ok();
|
||||
let user_prefs = profile.user_prefs().unwrap();
|
||||
|
||||
let pref = user_prefs.get("remote.log.level").unwrap();
|
||||
let value = match pref.value {
|
||||
PrefValue::String(ref s) => s,
|
||||
_ => panic!(),
|
||||
};
|
||||
for (i, ch) in value.chars().enumerate() {
|
||||
if i == 0 {
|
||||
assert!(ch.is_uppercase());
|
||||
} else {
|
||||
assert!(ch.is_lowercase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefs() {
|
||||
let marionette_settings = Default::default();
|
||||
|
||||
let encoded_profile = example_profile();
|
||||
let mut prefs: Map<String, Value> = Map::new();
|
||||
prefs.insert(
|
||||
"browser.display.background_color".into(),
|
||||
Value::String("#00ff00".into()),
|
||||
);
|
||||
|
||||
let mut firefox_opts = Map::new();
|
||||
firefox_opts.insert("profile".into(), encoded_profile);
|
||||
firefox_opts.insert("prefs".into(), Value::Object(prefs));
|
||||
|
||||
let mut caps = Map::new();
|
||||
caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
|
||||
|
||||
let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
|
||||
.expect("Valid profile and prefs");
|
||||
|
||||
let mut profile = match opts.profile {
|
||||
ProfileType::Path(profile) => profile,
|
||||
_ => panic!("Expected ProfileType::Path"),
|
||||
};
|
||||
|
||||
set_prefs(2828, &mut profile, true, opts.prefs, false).expect("set preferences");
|
||||
|
||||
let prefs_set = profile.user_prefs().expect("valid user preferences");
|
||||
println!("{:#?}", prefs_set.prefs);
|
||||
|
||||
assert_eq!(
|
||||
prefs_set.get("startup.homepage_welcome_url"),
|
||||
Some(&Pref::new("data:text/html,PASS"))
|
||||
);
|
||||
assert_eq!(
|
||||
prefs_set.get("browser.display.background_color"),
|
||||
Some(&Pref::new("#00ff00"))
|
||||
);
|
||||
assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pref_backup() {
|
||||
let mut profile = Profile::new(None).unwrap();
|
||||
|
||||
// Create some prefs in the profile
|
||||
let initial_prefs = profile.user_prefs().unwrap();
|
||||
initial_prefs.insert("geckodriver.example", Pref::new("example"));
|
||||
initial_prefs.write().unwrap();
|
||||
|
||||
let prefs_path = initial_prefs.path.clone();
|
||||
|
||||
let mut conflicting_backup_path = initial_prefs.path.clone();
|
||||
conflicting_backup_path.set_extension("geckodriver_backup");
|
||||
println!("{:?}", conflicting_backup_path);
|
||||
let mut file = File::create(&conflicting_backup_path).unwrap();
|
||||
file.write_all(b"test").unwrap();
|
||||
assert!(conflicting_backup_path.exists());
|
||||
|
||||
let mut initial_prefs_data = String::new();
|
||||
File::open(&prefs_path)
|
||||
.expect("Initial prefs exist")
|
||||
.read_to_string(&mut initial_prefs_data)
|
||||
.unwrap();
|
||||
|
||||
let backup = set_prefs(2828, &mut profile, true, vec![], false)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let user_prefs = profile.user_prefs().unwrap();
|
||||
|
||||
assert!(user_prefs.path.exists());
|
||||
let mut backup_path = user_prefs.path.clone();
|
||||
backup_path.set_extension("geckodriver_backup_1");
|
||||
|
||||
assert!(backup_path.exists());
|
||||
|
||||
// Ensure the actual prefs contain both the existing ones and the ones we added
|
||||
let pref = user_prefs.get("marionette.port").unwrap();
|
||||
assert_eq!(pref.value, PrefValue::Int(2828));
|
||||
|
||||
let pref = user_prefs.get("geckodriver.example").unwrap();
|
||||
assert_eq!(pref.value, PrefValue::String("example".into()));
|
||||
|
||||
// Ensure the backup prefs don't contain the new settings
|
||||
let mut backup_data = String::new();
|
||||
File::open(&backup_path)
|
||||
.expect("Backup prefs exist")
|
||||
.read_to_string(&mut backup_data)
|
||||
.unwrap();
|
||||
assert_eq!(backup_data, initial_prefs_data);
|
||||
|
||||
backup.restore();
|
||||
|
||||
assert!(!backup_path.exists());
|
||||
let mut final_prefs_data = String::new();
|
||||
File::open(&prefs_path)
|
||||
.expect("Initial prefs exist")
|
||||
.read_to_string(&mut final_prefs_data)
|
||||
.unwrap();
|
||||
assert_eq!(final_prefs_data, initial_prefs_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_read_marionette_port() {
|
||||
fn create_port_file(profile_path: &Path, data: &[u8]) {
|
||||
let port_path = profile_path.join("MarionetteActivePort");
|
||||
let mut file = File::create(&port_path).unwrap();
|
||||
file.write_all(data).unwrap();
|
||||
}
|
||||
|
||||
let profile_dir = tempdir().unwrap();
|
||||
let profile_path = profile_dir.path();
|
||||
assert_eq!(read_marionette_port(profile_path), None);
|
||||
assert_eq!(read_marionette_port(profile_path), None);
|
||||
create_port_file(profile_path, b"");
|
||||
assert_eq!(read_marionette_port(profile_path), None);
|
||||
create_port_file(profile_path, b"1234");
|
||||
assert_eq!(read_marionette_port(profile_path), Some(1234));
|
||||
create_port_file(profile_path, b"1234abc");
|
||||
assert_eq!(read_marionette_port(profile_path), None);
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde_json::Value;
|
||||
use std::fmt;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/build-info.rs"));
|
||||
|
||||
pub struct BuildInfo;
|
||||
|
||||
impl BuildInfo {
|
||||
pub fn version() -> &'static str {
|
||||
crate_version!()
|
||||
}
|
||||
|
||||
pub fn hash() -> Option<&'static str> {
|
||||
COMMIT_HASH
|
||||
}
|
||||
|
||||
pub fn date() -> Option<&'static str> {
|
||||
COMMIT_DATE
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BuildInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", BuildInfo::version())?;
|
||||
match (BuildInfo::hash(), BuildInfo::date()) {
|
||||
(Some(hash), Some(date)) => write!(f, " ({} {})", hash, date)?,
|
||||
(Some(hash), None) => write!(f, " ({})", hash)?,
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BuildInfo> for Value {
|
||||
fn from(_: BuildInfo) -> Value {
|
||||
Value::String(BuildInfo::version().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns build-time information about geckodriver.
|
||||
pub fn build_info() -> BuildInfo {
|
||||
BuildInfo {}
|
||||
}
|
||||
@ -1,339 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::logging;
|
||||
use hyper::Method;
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
use serde_json::{self, Value};
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use uuid::Uuid;
|
||||
use webdriver::command::{WebDriverCommand, WebDriverExtensionCommand};
|
||||
use webdriver::error::WebDriverResult;
|
||||
use webdriver::httpapi::WebDriverExtensionRoute;
|
||||
use webdriver::Parameters;
|
||||
|
||||
pub fn extension_routes() -> Vec<(Method, &'static str, GeckoExtensionRoute)> {
|
||||
vec![
|
||||
(
|
||||
Method::GET,
|
||||
"/session/{sessionId}/moz/context",
|
||||
GeckoExtensionRoute::GetContext,
|
||||
),
|
||||
(
|
||||
Method::POST,
|
||||
"/session/{sessionId}/moz/context",
|
||||
GeckoExtensionRoute::SetContext,
|
||||
),
|
||||
(
|
||||
Method::POST,
|
||||
"/session/{sessionId}/moz/addon/install",
|
||||
GeckoExtensionRoute::InstallAddon,
|
||||
),
|
||||
(
|
||||
Method::POST,
|
||||
"/session/{sessionId}/moz/addon/uninstall",
|
||||
GeckoExtensionRoute::UninstallAddon,
|
||||
),
|
||||
(
|
||||
Method::GET,
|
||||
"/session/{sessionId}/moz/screenshot/full",
|
||||
GeckoExtensionRoute::TakeFullScreenshot,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum GeckoExtensionRoute {
|
||||
GetContext,
|
||||
SetContext,
|
||||
InstallAddon,
|
||||
UninstallAddon,
|
||||
TakeFullScreenshot,
|
||||
}
|
||||
|
||||
impl WebDriverExtensionRoute for GeckoExtensionRoute {
|
||||
type Command = GeckoExtensionCommand;
|
||||
|
||||
fn command(
|
||||
&self,
|
||||
_params: &Parameters,
|
||||
body_data: &Value,
|
||||
) -> WebDriverResult<WebDriverCommand<GeckoExtensionCommand>> {
|
||||
use self::GeckoExtensionRoute::*;
|
||||
|
||||
let command = match *self {
|
||||
GetContext => GeckoExtensionCommand::GetContext,
|
||||
SetContext => {
|
||||
GeckoExtensionCommand::SetContext(serde_json::from_value(body_data.clone())?)
|
||||
}
|
||||
InstallAddon => {
|
||||
GeckoExtensionCommand::InstallAddon(serde_json::from_value(body_data.clone())?)
|
||||
}
|
||||
UninstallAddon => {
|
||||
GeckoExtensionCommand::UninstallAddon(serde_json::from_value(body_data.clone())?)
|
||||
}
|
||||
TakeFullScreenshot => GeckoExtensionCommand::TakeFullScreenshot,
|
||||
};
|
||||
|
||||
Ok(WebDriverCommand::Extension(command))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum GeckoExtensionCommand {
|
||||
GetContext,
|
||||
SetContext(GeckoContextParameters),
|
||||
InstallAddon(AddonInstallParameters),
|
||||
UninstallAddon(AddonUninstallParameters),
|
||||
TakeFullScreenshot,
|
||||
}
|
||||
|
||||
impl WebDriverExtensionCommand for GeckoExtensionCommand {
|
||||
fn parameters_json(&self) -> Option<Value> {
|
||||
use self::GeckoExtensionCommand::*;
|
||||
match self {
|
||||
GetContext => None,
|
||||
InstallAddon(x) => Some(serde_json::to_value(x).unwrap()),
|
||||
SetContext(x) => Some(serde_json::to_value(x).unwrap()),
|
||||
UninstallAddon(x) => Some(serde_json::to_value(x).unwrap()),
|
||||
TakeFullScreenshot => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AddonInstallParameters {
|
||||
pub path: String,
|
||||
pub temporary: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AddonInstallParameters {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Base64 {
|
||||
addon: String,
|
||||
temporary: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Path {
|
||||
path: String,
|
||||
temporary: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Helper {
|
||||
Base64(Base64),
|
||||
Path(Path),
|
||||
}
|
||||
|
||||
let params = match Helper::deserialize(deserializer)? {
|
||||
Helper::Path(ref mut data) => AddonInstallParameters {
|
||||
path: data.path.clone(),
|
||||
temporary: data.temporary,
|
||||
},
|
||||
Helper::Base64(ref mut data) => {
|
||||
let content = base64::decode(&data.addon).map_err(de::Error::custom)?;
|
||||
|
||||
let path = env::temp_dir()
|
||||
.as_path()
|
||||
.join(format!("addon-{}.xpi", Uuid::new_v4()));
|
||||
let mut xpi_file = File::create(&path).map_err(de::Error::custom)?;
|
||||
xpi_file
|
||||
.write(content.as_slice())
|
||||
.map_err(de::Error::custom)?;
|
||||
|
||||
let path = match path.to_str() {
|
||||
Some(path) => path.to_string(),
|
||||
None => return Err(de::Error::custom("could not write addon to file")),
|
||||
};
|
||||
|
||||
AddonInstallParameters {
|
||||
path,
|
||||
temporary: data.temporary,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AddonUninstallParameters {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GeckoContext {
|
||||
Content,
|
||||
Chrome,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GeckoContextParameters {
|
||||
pub context: GeckoContext,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct XblLocatorParameters {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
pub struct LogOptions {
|
||||
pub level: Option<logging::Level>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::test::assert_de;
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_invalid() {
|
||||
assert!(serde_json::from_str::<AddonInstallParameters>("").is_err());
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json!(null)).is_err());
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json!({})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_path_and_temporary() {
|
||||
let params = AddonInstallParameters {
|
||||
path: "/path/to.xpi".to_string(),
|
||||
temporary: Some(true),
|
||||
};
|
||||
assert_de(¶ms, json!({"path": "/path/to.xpi", "temporary": true}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_path() {
|
||||
let params = AddonInstallParameters {
|
||||
path: "/path/to.xpi".to_string(),
|
||||
temporary: None,
|
||||
};
|
||||
assert_de(¶ms, json!({"path": "/path/to.xpi"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_path_invalid_type() {
|
||||
let json = json!({"path": true, "temporary": true});
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_path_and_temporary_invalid_type() {
|
||||
let json = json!({"path": "/path/to.xpi", "temporary": "foo"});
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_addon() {
|
||||
let json = json!({"addon": "aGVsbG8=", "temporary": true});
|
||||
let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap();
|
||||
|
||||
assert_eq!(data.temporary, Some(true));
|
||||
let mut file = File::open(data.path).unwrap();
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).unwrap();
|
||||
assert_eq!(contents, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_addon_only() {
|
||||
let json = json!({"addon": "aGVsbG8="});
|
||||
let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap();
|
||||
|
||||
assert_eq!(data.temporary, None);
|
||||
let mut file = File::open(data.path).unwrap();
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).unwrap();
|
||||
assert_eq!(contents, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_addon_invalid_type() {
|
||||
let json = json!({"addon": true, "temporary": true});
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_addon_and_temporary_invalid_type() {
|
||||
let json = json!({"addon": "aGVsbG8=", "temporary": "foo"});
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_install_parameters_with_temporary_only() {
|
||||
let json = json!({"temporary": true});
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_install_parameters_with_both_path_and_addon() {
|
||||
let json = json!({
|
||||
"path": "/path/to.xpi",
|
||||
"addon": "aGVsbG8=",
|
||||
"temporary": true,
|
||||
});
|
||||
assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_uninstall_parameters_invalid() {
|
||||
assert!(serde_json::from_str::<AddonUninstallParameters>("").is_err());
|
||||
assert!(serde_json::from_value::<AddonUninstallParameters>(json!(null)).is_err());
|
||||
assert!(serde_json::from_value::<AddonUninstallParameters>(json!({})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_uninstall_parameters() {
|
||||
let params = AddonUninstallParameters {
|
||||
id: "foo".to_string(),
|
||||
};
|
||||
assert_de(¶ms, json!({"id": "foo"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_addon_uninstall_parameters_id_invalid_type() {
|
||||
let json = json!({"id": true});
|
||||
assert!(serde_json::from_value::<AddonUninstallParameters>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_gecko_context_parameters_content() {
|
||||
let params = GeckoContextParameters {
|
||||
context: GeckoContext::Content,
|
||||
};
|
||||
assert_de(¶ms, json!({"context": "content"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_gecko_context_parameters_chrome() {
|
||||
let params = GeckoContextParameters {
|
||||
context: GeckoContext::Chrome,
|
||||
};
|
||||
assert_de(¶ms, json!({"context": "chrome"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_gecko_context_parameters_context_invalid() {
|
||||
type P = GeckoContextParameters;
|
||||
assert!(serde_json::from_value::<P>(json!({})).is_err());
|
||||
assert!(serde_json::from_value::<P>(json!({ "context": null })).is_err());
|
||||
assert!(serde_json::from_value::<P>(json!({"context": "foo"})).is_err());
|
||||
}
|
||||
}
|
||||
@ -1,403 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Gecko-esque logger implementation for the [`log`] crate.
|
||||
//!
|
||||
//! The [`log`] crate provides a single logging API that abstracts over the
|
||||
//! actual logging implementation. This module uses the logging API
|
||||
//! to provide a log implementation that shares many aesthetical traits with
|
||||
//! [Log.sys.mjs] from Gecko.
|
||||
//!
|
||||
//! Using the [`error!`], [`warn!`], [`info!`], [`debug!`], and
|
||||
//! [`trace!`] macros from `log` will output a timestamp field, followed by the
|
||||
//! log level, and then the message. The fields are separated by a tab
|
||||
//! character, making the output suitable for further text processing with
|
||||
//! `awk(1)`.
|
||||
//!
|
||||
//! This module shares the same API as `log`, except it provides additional
|
||||
//! entry functions [`init`] and [`init_with_level`] and additional log levels
|
||||
//! `Level::Fatal` and `Level::Config`. Converting these into the
|
||||
//! [`log::Level`] is lossy so that `Level::Fatal` becomes `log::Level::Error`
|
||||
//! and `Level::Config` becomes `log::Level::Debug`.
|
||||
//!
|
||||
//! [`log`]: https://docs.rs/log/newest/log/
|
||||
//! [Log.sys.mjs]: https://searchfox.org/mozilla-central/source/toolkit/modules/Log.sys.mjs
|
||||
//! [`error!`]: https://docs.rs/log/newest/log/macro.error.html
|
||||
//! [`warn!`]: https://docs.rs/log/newest/log/macro.warn.html
|
||||
//! [`info!`]: https://docs.rs/log/newest/log/macro.info.html
|
||||
//! [`debug!`]: https://docs.rs/log/newest/log/macro.debug.html
|
||||
//! [`trace!`]: https://docs.rs/log/newest/log/macro.trace.html
|
||||
//! [`init`]: fn.init.html
|
||||
//! [`init_with_level`]: fn.init_with_level.html
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::str;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use mozprofile::preferences::Pref;
|
||||
|
||||
static LOG_TRUNCATE: AtomicBool = AtomicBool::new(true);
|
||||
static MAX_LOG_LEVEL: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
const MAX_STRING_LENGTH: usize = 250;
|
||||
|
||||
const LOGGED_TARGETS: &[&str] = &[
|
||||
"geckodriver",
|
||||
"mozdevice",
|
||||
"mozprofile",
|
||||
"mozrunner",
|
||||
"mozversion",
|
||||
"webdriver",
|
||||
];
|
||||
|
||||
/// Logger levels from [Log.sys.mjs].
|
||||
///
|
||||
/// [Log.sys.mjs]: https://searchfox.org/mozilla-central/source/toolkit/modules/Log.sys.mjs
|
||||
#[repr(usize)]
|
||||
#[derive(Clone, Copy, Eq, Debug, Hash, PartialEq)]
|
||||
pub enum Level {
|
||||
Fatal = 70,
|
||||
Error = 60,
|
||||
Warn = 50,
|
||||
Info = 40,
|
||||
Config = 30,
|
||||
Debug = 20,
|
||||
Trace = 10,
|
||||
}
|
||||
|
||||
impl From<usize> for Level {
|
||||
fn from(n: usize) -> Level {
|
||||
use self::Level::*;
|
||||
match n {
|
||||
70 => Fatal,
|
||||
60 => Error,
|
||||
50 => Warn,
|
||||
40 => Info,
|
||||
30 => Config,
|
||||
20 => Debug,
|
||||
10 => Trace,
|
||||
_ => Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Level {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::Level::*;
|
||||
let s = match *self {
|
||||
Fatal => "FATAL",
|
||||
Error => "ERROR",
|
||||
Warn => "WARN",
|
||||
Info => "INFO",
|
||||
Config => "CONFIG",
|
||||
Debug => "DEBUG",
|
||||
Trace => "TRACE",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for Level {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Level, ()> {
|
||||
use self::Level::*;
|
||||
match s.to_lowercase().as_ref() {
|
||||
"fatal" => Ok(Fatal),
|
||||
"error" => Ok(Error),
|
||||
"warn" => Ok(Warn),
|
||||
"info" => Ok(Info),
|
||||
"config" => Ok(Config),
|
||||
"debug" => Ok(Debug),
|
||||
"trace" => Ok(Trace),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Level> for log::Level {
|
||||
fn from(level: Level) -> log::Level {
|
||||
use self::Level::*;
|
||||
match level {
|
||||
Fatal | Error => log::Level::Error,
|
||||
Warn => log::Level::Warn,
|
||||
Info => log::Level::Info,
|
||||
Config | Debug => log::Level::Debug,
|
||||
Trace => log::Level::Trace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Level> for Pref {
|
||||
fn from(level: Level) -> Pref {
|
||||
use self::Level::*;
|
||||
Pref::new(match level {
|
||||
Fatal => "Fatal",
|
||||
Error => "Error",
|
||||
Warn => "Warn",
|
||||
Info => "Info",
|
||||
Config => "Config",
|
||||
Debug => "Debug",
|
||||
Trace => "Trace",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<log::Level> for Level {
|
||||
fn from(log_level: log::Level) -> Level {
|
||||
use log::Level::*;
|
||||
match log_level {
|
||||
Error => Level::Error,
|
||||
Warn => Level::Warn,
|
||||
Info => Level::Info,
|
||||
Debug => Level::Debug,
|
||||
Trace => Level::Trace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Logger;
|
||||
|
||||
impl log::Log for Logger {
|
||||
fn enabled(&self, meta: &log::Metadata) -> bool {
|
||||
LOGGED_TARGETS.iter().any(|&x| meta.target().starts_with(x))
|
||||
&& meta.level() <= log::max_level()
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
if let Some((s1, s2)) = truncate_message(record.args()) {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{} ... {}",
|
||||
format_ts(chrono::Local::now()),
|
||||
record.target(),
|
||||
record.level(),
|
||||
s1,
|
||||
s2
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}",
|
||||
format_ts(chrono::Local::now()),
|
||||
record.target(),
|
||||
record.level(),
|
||||
record.args()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialises the logging subsystem with the default log level.
|
||||
pub fn init(truncate: bool) -> Result<(), log::SetLoggerError> {
|
||||
init_with_level(Level::Info, truncate)
|
||||
}
|
||||
|
||||
/// Initialises the logging subsystem.
|
||||
pub fn init_with_level(level: Level, truncate: bool) -> Result<(), log::SetLoggerError> {
|
||||
let logger = Logger {};
|
||||
set_max_level(level);
|
||||
set_truncate(truncate);
|
||||
log::set_boxed_logger(Box::new(logger))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the current maximum log level.
|
||||
pub fn max_level() -> Level {
|
||||
MAX_LOG_LEVEL.load(Ordering::Relaxed).into()
|
||||
}
|
||||
|
||||
/// Sets the global maximum log level.
|
||||
pub fn set_max_level(level: Level) {
|
||||
MAX_LOG_LEVEL.store(level as usize, Ordering::SeqCst);
|
||||
|
||||
let slevel: log::Level = level.into();
|
||||
log::set_max_level(slevel.to_level_filter())
|
||||
}
|
||||
|
||||
/// Sets the global maximum log level.
|
||||
pub fn set_truncate(truncate: bool) {
|
||||
LOG_TRUNCATE.store(truncate, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Returns the truncation flag.
|
||||
pub fn truncate() -> bool {
|
||||
LOG_TRUNCATE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Produces a 13-digit Unix Epoch timestamp similar to Gecko.
|
||||
fn format_ts(ts: chrono::DateTime<chrono::Local>) -> String {
|
||||
format!("{}{:03}", ts.timestamp(), ts.timestamp_subsec_millis())
|
||||
}
|
||||
|
||||
/// Truncate a log message if it's too long
|
||||
fn truncate_message(args: &fmt::Arguments) -> Option<(String, String)> {
|
||||
// Don't truncate the message if requested.
|
||||
if !truncate() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let message = format!("{}", args);
|
||||
let chars = message.graphemes(true).collect::<Vec<&str>>();
|
||||
|
||||
if chars.len() > MAX_STRING_LENGTH {
|
||||
let middle: usize = MAX_STRING_LENGTH / 2;
|
||||
let s1 = chars[0..middle].concat();
|
||||
let s2 = chars[chars.len() - middle..].concat();
|
||||
Some((s1, s2))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use mozprofile::preferences::{Pref, PrefValue};
|
||||
|
||||
lazy_static! {
|
||||
static ref LEVEL_MUTEX: Mutex<()> = Mutex::new(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_repr() {
|
||||
assert_eq!(Level::Fatal as usize, 70);
|
||||
assert_eq!(Level::Error as usize, 60);
|
||||
assert_eq!(Level::Warn as usize, 50);
|
||||
assert_eq!(Level::Info as usize, 40);
|
||||
assert_eq!(Level::Config as usize, 30);
|
||||
assert_eq!(Level::Debug as usize, 20);
|
||||
assert_eq!(Level::Trace as usize, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_from_log() {
|
||||
assert_eq!(Level::from(log::Level::Error), Level::Error);
|
||||
assert_eq!(Level::from(log::Level::Warn), Level::Warn);
|
||||
assert_eq!(Level::from(log::Level::Info), Level::Info);
|
||||
assert_eq!(Level::from(log::Level::Debug), Level::Debug);
|
||||
assert_eq!(Level::from(log::Level::Trace), Level::Trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_into_log() {
|
||||
assert_eq!(Into::<log::Level>::into(Level::Fatal), log::Level::Error);
|
||||
assert_eq!(Into::<log::Level>::into(Level::Error), log::Level::Error);
|
||||
assert_eq!(Into::<log::Level>::into(Level::Warn), log::Level::Warn);
|
||||
assert_eq!(Into::<log::Level>::into(Level::Info), log::Level::Info);
|
||||
assert_eq!(Into::<log::Level>::into(Level::Config), log::Level::Debug);
|
||||
assert_eq!(Into::<log::Level>::into(Level::Debug), log::Level::Debug);
|
||||
assert_eq!(Into::<log::Level>::into(Level::Trace), log::Level::Trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_into_pref() {
|
||||
let tests = [
|
||||
(Level::Fatal, "Fatal"),
|
||||
(Level::Error, "Error"),
|
||||
(Level::Warn, "Warn"),
|
||||
(Level::Info, "Info"),
|
||||
(Level::Config, "Config"),
|
||||
(Level::Debug, "Debug"),
|
||||
(Level::Trace, "Trace"),
|
||||
];
|
||||
|
||||
for &(lvl, s) in tests.iter() {
|
||||
let expected = Pref {
|
||||
value: PrefValue::String(s.to_string()),
|
||||
sticky: false,
|
||||
};
|
||||
assert_eq!(Into::<Pref>::into(lvl), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_from_str() {
|
||||
assert_eq!(Level::from_str("fatal"), Ok(Level::Fatal));
|
||||
assert_eq!(Level::from_str("error"), Ok(Level::Error));
|
||||
assert_eq!(Level::from_str("warn"), Ok(Level::Warn));
|
||||
assert_eq!(Level::from_str("info"), Ok(Level::Info));
|
||||
assert_eq!(Level::from_str("config"), Ok(Level::Config));
|
||||
assert_eq!(Level::from_str("debug"), Ok(Level::Debug));
|
||||
assert_eq!(Level::from_str("trace"), Ok(Level::Trace));
|
||||
|
||||
assert_eq!(Level::from_str("INFO"), Ok(Level::Info));
|
||||
|
||||
assert!(Level::from_str("foo").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_to_str() {
|
||||
assert_eq!(Level::Fatal.to_string(), "FATAL");
|
||||
assert_eq!(Level::Error.to_string(), "ERROR");
|
||||
assert_eq!(Level::Warn.to_string(), "WARN");
|
||||
assert_eq!(Level::Info.to_string(), "INFO");
|
||||
assert_eq!(Level::Config.to_string(), "CONFIG");
|
||||
assert_eq!(Level::Debug.to_string(), "DEBUG");
|
||||
assert_eq!(Level::Trace.to_string(), "TRACE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_level() {
|
||||
let _guard = LEVEL_MUTEX.lock();
|
||||
set_max_level(Level::Info);
|
||||
assert_eq!(max_level(), Level::Info);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_max_level() {
|
||||
let _guard = LEVEL_MUTEX.lock();
|
||||
set_max_level(Level::Error);
|
||||
assert_eq!(max_level(), Level::Error);
|
||||
set_max_level(Level::Fatal);
|
||||
assert_eq!(max_level(), Level::Fatal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_with_level() {
|
||||
let _guard = LEVEL_MUTEX.lock();
|
||||
init_with_level(Level::Debug, false).unwrap();
|
||||
assert_eq!(max_level(), Level::Debug);
|
||||
assert!(init_with_level(Level::Warn, false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_ts() {
|
||||
let ts = chrono::Local::now();
|
||||
let s = format_ts(ts);
|
||||
assert_eq!(s.len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate() {
|
||||
let short_message = (0..MAX_STRING_LENGTH).map(|_| "x").collect::<String>();
|
||||
// A message up to MAX_STRING_LENGTH is not truncated
|
||||
assert_eq!(truncate_message(&format_args!("{}", short_message)), None);
|
||||
|
||||
let long_message = (0..MAX_STRING_LENGTH + 1).map(|_| "x").collect::<String>();
|
||||
let part = (0..MAX_STRING_LENGTH / 2).map(|_| "x").collect::<String>();
|
||||
|
||||
// A message longer than MAX_STRING_LENGTH is not truncated if requested
|
||||
set_truncate(false);
|
||||
assert_eq!(truncate_message(&format_args!("{}", long_message)), None);
|
||||
|
||||
// A message longer than MAX_STRING_LENGTH is truncated if requested
|
||||
set_truncate(true);
|
||||
assert_eq!(
|
||||
truncate_message(&format_args!("{}", long_message)),
|
||||
Some((part.to_owned(), part))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,549 +0,0 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
extern crate hyper;
|
||||
extern crate marionette as marionette_rs;
|
||||
extern crate mozdevice;
|
||||
extern crate mozprofile;
|
||||
extern crate mozrunner;
|
||||
extern crate mozversion;
|
||||
extern crate regex;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate serde_yaml;
|
||||
extern crate tempfile;
|
||||
extern crate url;
|
||||
extern crate uuid;
|
||||
extern crate webdriver;
|
||||
extern crate zip;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use std::result;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::{AppSettings, Arg, Command};
|
||||
|
||||
macro_rules! try_opt {
|
||||
($expr:expr, $err_type:expr, $err_msg:expr) => {{
|
||||
match $expr {
|
||||
Some(x) => x,
|
||||
None => return Err(WebDriverError::new($err_type, $err_msg)),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
mod android;
|
||||
mod browser;
|
||||
mod build;
|
||||
mod capabilities;
|
||||
mod command;
|
||||
mod logging;
|
||||
mod marionette;
|
||||
mod prefs;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
||||
use crate::command::extension_routes;
|
||||
use crate::logging::Level;
|
||||
use crate::marionette::{MarionetteHandler, MarionetteSettings};
|
||||
use mozdevice::AndroidStorageInput;
|
||||
use url::{Host, Url};
|
||||
|
||||
const EXIT_SUCCESS: i32 = 0;
|
||||
const EXIT_USAGE: i32 = 64;
|
||||
const EXIT_UNAVAILABLE: i32 = 69;
|
||||
|
||||
enum FatalError {
|
||||
Parsing(clap::Error),
|
||||
Usage(String),
|
||||
Server(io::Error),
|
||||
}
|
||||
|
||||
impl FatalError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
use FatalError::*;
|
||||
match *self {
|
||||
Parsing(_) | Usage(_) => EXIT_USAGE,
|
||||
Server(_) => EXIT_UNAVAILABLE,
|
||||
}
|
||||
}
|
||||
|
||||
fn help_included(&self) -> bool {
|
||||
matches!(*self, FatalError::Parsing(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<clap::Error> for FatalError {
|
||||
fn from(err: clap::Error) -> FatalError {
|
||||
FatalError::Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for FatalError {
|
||||
fn from(err: io::Error) -> FatalError {
|
||||
FatalError::Server(err)
|
||||
}
|
||||
}
|
||||
|
||||
// harmonise error message from clap to avoid duplicate "error:" prefix
|
||||
impl fmt::Display for FatalError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use FatalError::*;
|
||||
let s = match *self {
|
||||
Parsing(ref err) => err.to_string(),
|
||||
Usage(ref s) => format!("error: {}", s),
|
||||
Server(ref err) => format!("error: {}", err),
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! usage {
|
||||
($msg:expr) => {
|
||||
return Err(FatalError::Usage($msg.to_string()))
|
||||
};
|
||||
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
return Err(FatalError::Usage(format!($fmt, $($arg)+)))
|
||||
};
|
||||
}
|
||||
|
||||
type ProgramResult<T> = result::Result<T, FatalError>;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Operation {
|
||||
Help,
|
||||
Version,
|
||||
Server {
|
||||
log_level: Option<Level>,
|
||||
log_truncate: bool,
|
||||
address: SocketAddr,
|
||||
allow_hosts: Vec<Host>,
|
||||
allow_origins: Vec<Url>,
|
||||
settings: MarionetteSettings,
|
||||
deprecated_storage_arg: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Get a socket address from the provided host and port
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `webdriver_host` - The hostname on which the server will listen
|
||||
/// * `webdriver_port` - The port on which the server will listen
|
||||
///
|
||||
/// When the host and port resolve to multiple addresses, prefer
|
||||
/// IPv4 addresses vs IPv6.
|
||||
fn server_address(webdriver_host: &str, webdriver_port: u16) -> ProgramResult<SocketAddr> {
|
||||
let mut socket_addrs = match format!("{}:{}", webdriver_host, webdriver_port).to_socket_addrs()
|
||||
{
|
||||
Ok(addrs) => addrs.collect::<Vec<_>>(),
|
||||
Err(e) => usage!("{}: {}:{}", e, webdriver_host, webdriver_port),
|
||||
};
|
||||
if socket_addrs.is_empty() {
|
||||
usage!(
|
||||
"Unable to resolve host: {}:{}",
|
||||
webdriver_host,
|
||||
webdriver_port
|
||||
)
|
||||
}
|
||||
// Prefer ipv4 address
|
||||
socket_addrs.sort_by(|a, b| {
|
||||
let a_val = i32::from(!a.ip().is_ipv4());
|
||||
let b_val = i32::from(!b.ip().is_ipv4());
|
||||
a_val.partial_cmp(&b_val).expect("Comparison failed")
|
||||
});
|
||||
Ok(socket_addrs.remove(0))
|
||||
}
|
||||
|
||||
/// Parse a given string into a Host
|
||||
fn parse_hostname(webdriver_host: &str) -> Result<Host, url::ParseError> {
|
||||
let host_str = if let Ok(ip_addr) = IpAddr::from_str(webdriver_host) {
|
||||
// In this case we have an IP address as the host
|
||||
if ip_addr.is_ipv6() {
|
||||
// Convert to quoted form
|
||||
format!("[{}]", &webdriver_host)
|
||||
} else {
|
||||
webdriver_host.into()
|
||||
}
|
||||
} else {
|
||||
webdriver_host.into()
|
||||
};
|
||||
|
||||
Host::parse(&host_str)
|
||||
}
|
||||
|
||||
/// Get a list of default hostnames to allow
|
||||
///
|
||||
/// This only covers domain names, not IP addresses, since IP adresses
|
||||
/// are always accepted.
|
||||
fn get_default_allowed_hosts(ip: IpAddr) -> Vec<Result<Host, url::ParseError>> {
|
||||
let localhost_is_loopback = ("localhost".to_string(), 80)
|
||||
.to_socket_addrs()
|
||||
.map(|addr_iter| {
|
||||
addr_iter
|
||||
.map(|addr| addr.ip())
|
||||
.filter(|ip| ip.is_loopback())
|
||||
})
|
||||
.iter()
|
||||
.len()
|
||||
> 0;
|
||||
if ip.is_loopback() && localhost_is_loopback {
|
||||
vec![Host::parse("localhost")]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn get_allowed_hosts(
|
||||
host: Host,
|
||||
allow_hosts: Option<clap::Values>,
|
||||
) -> Result<Vec<Host>, url::ParseError> {
|
||||
allow_hosts
|
||||
.map(|hosts| hosts.map(Host::parse).collect::<Vec<_>>())
|
||||
.unwrap_or_else(|| match host {
|
||||
Host::Domain(_) => {
|
||||
vec![Ok(host.clone())]
|
||||
}
|
||||
Host::Ipv4(ip) => get_default_allowed_hosts(IpAddr::V4(ip)),
|
||||
Host::Ipv6(ip) => get_default_allowed_hosts(IpAddr::V6(ip)),
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<Host>, url::ParseError>>()
|
||||
}
|
||||
|
||||
fn get_allowed_origins(allow_origins: Option<clap::Values>) -> Result<Vec<Url>, url::ParseError> {
|
||||
allow_origins
|
||||
.map(|origins| {
|
||||
origins
|
||||
.map(Url::parse)
|
||||
.collect::<Result<Vec<Url>, url::ParseError>>()
|
||||
})
|
||||
.unwrap_or_else(|| Ok(vec![]))
|
||||
}
|
||||
|
||||
fn parse_args(cmd: &mut Command) -> ProgramResult<Operation> {
|
||||
let args = cmd.try_get_matches_from_mut(env::args())?;
|
||||
|
||||
if args.is_present("help") {
|
||||
return Ok(Operation::Help);
|
||||
} else if args.is_present("version") {
|
||||
return Ok(Operation::Version);
|
||||
}
|
||||
|
||||
let log_level = if args.is_present("log_level") {
|
||||
Level::from_str(args.value_of("log_level").unwrap()).ok()
|
||||
} else {
|
||||
Some(match args.occurrences_of("verbosity") {
|
||||
0 => Level::Info,
|
||||
1 => Level::Debug,
|
||||
_ => Level::Trace,
|
||||
})
|
||||
};
|
||||
|
||||
let webdriver_host = args.value_of("webdriver_host").unwrap();
|
||||
let webdriver_port = {
|
||||
let s = args.value_of("webdriver_port").unwrap();
|
||||
match u16::from_str(s) {
|
||||
Ok(n) => n,
|
||||
Err(e) => usage!("invalid --port: {}: {}", e, s),
|
||||
}
|
||||
};
|
||||
|
||||
let android_storage = args
|
||||
.value_of_t::<AndroidStorageInput>("android_storage")
|
||||
.unwrap_or(AndroidStorageInput::Auto);
|
||||
|
||||
let binary = args.value_of("binary").map(PathBuf::from);
|
||||
|
||||
let profile_root = args.value_of("profile_root").map(PathBuf::from);
|
||||
|
||||
// Try to create a temporary directory on startup to check that the directory exists and is writable
|
||||
{
|
||||
let tmp_dir = if let Some(ref tmp_root) = profile_root {
|
||||
tempfile::tempdir_in(tmp_root)
|
||||
} else {
|
||||
tempfile::tempdir()
|
||||
};
|
||||
if tmp_dir.is_err() {
|
||||
usage!("Unable to write to temporary directory; consider --profile-root with a writeable directory")
|
||||
}
|
||||
}
|
||||
|
||||
let marionette_host = args.value_of("marionette_host").unwrap();
|
||||
let marionette_port = match args.value_of("marionette_port") {
|
||||
Some(s) => match u16::from_str(s) {
|
||||
Ok(n) => Some(n),
|
||||
Err(e) => usage!("invalid --marionette-port: {}", e),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
// For Android the port on the device must be the same as the one on the
|
||||
// host. For now default to 9222, which is the default for --remote-debugging-port.
|
||||
let websocket_port = match args.value_of("websocket_port") {
|
||||
Some(s) => match u16::from_str(s) {
|
||||
Ok(n) => n,
|
||||
Err(e) => usage!("invalid --websocket-port: {}", e),
|
||||
},
|
||||
None => 9222,
|
||||
};
|
||||
|
||||
let host = match parse_hostname(webdriver_host) {
|
||||
Ok(name) => name,
|
||||
Err(e) => usage!("invalid --host {}: {}", webdriver_host, e),
|
||||
};
|
||||
|
||||
let allow_hosts = match get_allowed_hosts(host, args.values_of("allow_hosts")) {
|
||||
Ok(hosts) => hosts,
|
||||
Err(e) => usage!("invalid --allow-hosts {}", e),
|
||||
};
|
||||
|
||||
let allow_origins = match get_allowed_origins(args.values_of("allow_origins")) {
|
||||
Ok(origins) => origins,
|
||||
Err(e) => usage!("invalid --allow-origins {}", e),
|
||||
};
|
||||
|
||||
let address = server_address(webdriver_host, webdriver_port)?;
|
||||
|
||||
let settings = MarionetteSettings {
|
||||
binary,
|
||||
profile_root,
|
||||
connect_existing: args.is_present("connect_existing"),
|
||||
host: marionette_host.into(),
|
||||
port: marionette_port,
|
||||
websocket_port,
|
||||
allow_hosts: allow_hosts.clone(),
|
||||
allow_origins: allow_origins.clone(),
|
||||
jsdebugger: args.is_present("jsdebugger"),
|
||||
android_storage,
|
||||
};
|
||||
Ok(Operation::Server {
|
||||
log_level,
|
||||
log_truncate: !args.is_present("log_no_truncate"),
|
||||
allow_hosts,
|
||||
allow_origins,
|
||||
address,
|
||||
settings,
|
||||
deprecated_storage_arg: args.is_present("android_storage"),
|
||||
})
|
||||
}
|
||||
|
||||
fn inner_main(cmd: &mut Command) -> ProgramResult<()> {
|
||||
match parse_args(cmd)? {
|
||||
Operation::Help => print_help(cmd),
|
||||
Operation::Version => print_version(),
|
||||
|
||||
Operation::Server {
|
||||
log_level,
|
||||
log_truncate,
|
||||
address,
|
||||
allow_hosts,
|
||||
allow_origins,
|
||||
settings,
|
||||
deprecated_storage_arg,
|
||||
} => {
|
||||
if let Some(ref level) = log_level {
|
||||
logging::init_with_level(*level, log_truncate).unwrap();
|
||||
} else {
|
||||
logging::init(log_truncate).unwrap();
|
||||
}
|
||||
|
||||
if deprecated_storage_arg {
|
||||
warn!("--android-storage argument is deprecated and will be removed soon.");
|
||||
};
|
||||
|
||||
let handler = MarionetteHandler::new(settings);
|
||||
let listening = webdriver::server::start(
|
||||
address,
|
||||
allow_hosts,
|
||||
allow_origins,
|
||||
handler,
|
||||
extension_routes(),
|
||||
)?;
|
||||
info!("Listening on {}", listening.socket);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
use std::process::exit;
|
||||
|
||||
let mut cmd = make_command();
|
||||
|
||||
// use std::process:Termination when it graduates
|
||||
exit(match inner_main(&mut cmd) {
|
||||
Ok(_) => EXIT_SUCCESS,
|
||||
|
||||
Err(e) => {
|
||||
eprintln!("{}: {}", get_program_name(), e);
|
||||
if !e.help_included() {
|
||||
print_help(&mut cmd);
|
||||
}
|
||||
|
||||
e.exit_code()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn make_command<'a>() -> Command<'a> {
|
||||
Command::new(format!("geckodriver {}", build::build_info()))
|
||||
.setting(AppSettings::NoAutoHelp)
|
||||
.setting(AppSettings::NoAutoVersion)
|
||||
.about("WebDriver implementation for Firefox")
|
||||
.arg(
|
||||
Arg::new("webdriver_host")
|
||||
.long("host")
|
||||
.takes_value(true)
|
||||
.value_name("HOST")
|
||||
.default_value("127.0.0.1")
|
||||
.help("Host IP to use for WebDriver server"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("webdriver_port")
|
||||
.short('p')
|
||||
.long("port")
|
||||
.takes_value(true)
|
||||
.value_name("PORT")
|
||||
.default_value("4444")
|
||||
.help("Port to use for WebDriver server"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("binary")
|
||||
.short('b')
|
||||
.long("binary")
|
||||
.takes_value(true)
|
||||
.value_name("BINARY")
|
||||
.help("Path to the Firefox binary"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("marionette_host")
|
||||
.long("marionette-host")
|
||||
.takes_value(true)
|
||||
.value_name("HOST")
|
||||
.default_value("127.0.0.1")
|
||||
.help("Host to use to connect to Gecko"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("marionette_port")
|
||||
.long("marionette-port")
|
||||
.takes_value(true)
|
||||
.value_name("PORT")
|
||||
.help("Port to use to connect to Gecko [default: system-allocated port]"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("websocket_port")
|
||||
.long("websocket-port")
|
||||
.takes_value(true)
|
||||
.value_name("PORT")
|
||||
.conflicts_with("connect_existing")
|
||||
.help("Port to use to connect to WebDriver BiDi [default: 9222]"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("connect_existing")
|
||||
.long("connect-existing")
|
||||
.requires("marionette_port")
|
||||
.help("Connect to an existing Firefox instance"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("jsdebugger")
|
||||
.long("jsdebugger")
|
||||
.help("Attach browser toolbox debugger for Firefox"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("verbosity")
|
||||
.multiple_occurrences(true)
|
||||
.conflicts_with("log_level")
|
||||
.short('v')
|
||||
.help("Log level verbosity (-v for debug and -vv for trace level)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("log_level")
|
||||
.long("log")
|
||||
.takes_value(true)
|
||||
.value_name("LEVEL")
|
||||
.possible_values(["fatal", "error", "warn", "info", "config", "debug", "trace"])
|
||||
.help("Set Gecko log level"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("log_no_truncate")
|
||||
.long("log-no-truncate")
|
||||
.help("Disable truncation of long log lines"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("help")
|
||||
.short('h')
|
||||
.long("help")
|
||||
.help("Prints this message"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("version")
|
||||
.short('V')
|
||||
.long("version")
|
||||
.help("Prints version and copying information"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("profile_root")
|
||||
.long("profile-root")
|
||||
.takes_value(true)
|
||||
.value_name("PROFILE_ROOT")
|
||||
.help("Directory in which to create profiles. Defaults to the system temporary directory."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("android_storage")
|
||||
.long("android-storage")
|
||||
.possible_values(["auto", "app", "internal", "sdcard"])
|
||||
.value_name("ANDROID_STORAGE")
|
||||
.help("Selects storage location to be used for test data (deprecated)."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("allow_hosts")
|
||||
.long("allow-hosts")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.value_name("ALLOW_HOSTS")
|
||||
.help("List of hostnames to allow. By default the value of --host is allowed, and in addition if that's a well known local address, other variations on well known local addresses are allowed. If --allow-hosts is provided only exactly those hosts are allowed."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("allow_origins")
|
||||
.long("allow-origins")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.value_name("ALLOW_ORIGINS")
|
||||
.help("List of request origins to allow. These must be formatted as scheme://host:port. By default any request with an origin header is rejected. If --allow-origins is provided then only exactly those origins are allowed."),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_program_name() -> String {
|
||||
env::args().next().unwrap()
|
||||
}
|
||||
|
||||
fn print_help(cmd: &mut Command) {
|
||||
cmd.print_help().ok();
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_version() {
|
||||
println!("geckodriver {}", build::build_info());
|
||||
println!();
|
||||
println!("The source code of this program is available from");
|
||||
println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central.");
|
||||
println!();
|
||||
println!("This program is subject to the terms of the Mozilla Public License 2.0.");
|
||||
println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.");
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use mozprofile::preferences::Pref;
|
||||
|
||||
// ALL CHANGES TO THIS FILE MUST HAVE REVIEW FROM A GECKODRIVER PEER!
|
||||
//
|
||||
// Please refer to INSTRUCTIONS TO ADD A NEW PREFERENCE in
|
||||
// remote/shared/RecommendedPreferences.sys.mjs
|
||||
//
|
||||
// Note: geckodriver is used out-of-tree with various builds of Firefox.
|
||||
// Removing a preference from this file will cause regressions,
|
||||
// so please be careful and get review from a Testing :: geckodriver peer
|
||||
// before you make any changes to this file.
|
||||
lazy_static! {
|
||||
pub static ref DEFAULT: Vec<(&'static str, Pref)> = vec![
|
||||
// Make sure Shield doesn't hit the network.
|
||||
("app.normandy.api_url", Pref::new("")),
|
||||
|
||||
// Disable Firefox old build background check
|
||||
("app.update.checkInstallTime", Pref::new(false)),
|
||||
|
||||
// Disable automatically upgrading Firefox
|
||||
//
|
||||
// Note: Possible update tests could reset or flip the value to allow
|
||||
// updates to be downloaded and applied.
|
||||
("app.update.disabledForTesting", Pref::new(true)),
|
||||
|
||||
// Enable the dump function, which sends messages to the system
|
||||
// console
|
||||
("browser.dom.window.dump.enabled", Pref::new(true)),
|
||||
("devtools.console.stdout.chrome", Pref::new(true)),
|
||||
|
||||
// Disable safebrowsing components
|
||||
("browser.safebrowsing.blockedURIs.enabled", Pref::new(false)),
|
||||
("browser.safebrowsing.downloads.enabled", Pref::new(false)),
|
||||
("browser.safebrowsing.passwords.enabled", Pref::new(false)),
|
||||
("browser.safebrowsing.malware.enabled", Pref::new(false)),
|
||||
("browser.safebrowsing.phishing.enabled", Pref::new(false)),
|
||||
|
||||
// Do not restore the last open set of tabs if the browser crashed
|
||||
("browser.sessionstore.resume_from_crash", Pref::new(false)),
|
||||
|
||||
// Skip check for default browser on startup
|
||||
("browser.shell.checkDefaultBrowser", Pref::new(false)),
|
||||
|
||||
// Do not redirect user when a milestone upgrade of Firefox
|
||||
// is detected
|
||||
("browser.startup.homepage_override.mstone", Pref::new("ignore")),
|
||||
|
||||
// Start with a blank page (about:blank)
|
||||
("browser.startup.page", Pref::new(0)),
|
||||
|
||||
// Disable the UI tour
|
||||
("browser.uitour.enabled", Pref::new(false)),
|
||||
|
||||
// Do not warn on quitting Firefox
|
||||
("browser.warnOnQuit", Pref::new(false)),
|
||||
|
||||
// Defensively disable data reporting systems
|
||||
("datareporting.healthreport.documentServerURI", Pref::new("http://%(server)s/dummy/healthreport/")),
|
||||
("datareporting.healthreport.logging.consoleEnabled", Pref::new(false)),
|
||||
("datareporting.healthreport.service.enabled", Pref::new(false)),
|
||||
("datareporting.healthreport.service.firstRun", Pref::new(false)),
|
||||
("datareporting.healthreport.uploadEnabled", Pref::new(false)),
|
||||
|
||||
// Do not show datareporting policy notifications which can
|
||||
// interfere with tests
|
||||
("datareporting.policy.dataSubmissionEnabled", Pref::new(false)),
|
||||
("datareporting.policy.dataSubmissionPolicyBypassNotification", Pref::new(true)),
|
||||
|
||||
// Disable the ProcessHangMonitor
|
||||
("dom.ipc.reportProcessHangs", Pref::new(false)),
|
||||
|
||||
// Only load extensions from the application and user profile
|
||||
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
|
||||
("extensions.autoDisableScopes", Pref::new(0)),
|
||||
("extensions.enabledScopes", Pref::new(5)),
|
||||
|
||||
// Disable intalling any distribution extensions or add-ons
|
||||
("extensions.installDistroAddons", Pref::new(false)),
|
||||
|
||||
// Turn off extension updates so they do not bother tests
|
||||
("extensions.update.enabled", Pref::new(false)),
|
||||
("extensions.update.notifyUser", Pref::new(false)),
|
||||
|
||||
// Allow the application to have focus even it runs in the
|
||||
// background
|
||||
("focusmanager.testmode", Pref::new(true)),
|
||||
|
||||
// Disable useragent updates
|
||||
("general.useragent.updates.enabled", Pref::new(false)),
|
||||
|
||||
// Always use network provider for geolocation tests so we bypass
|
||||
// the macOS dialog raised by the corelocation provider
|
||||
("geo.provider.testing", Pref::new(true)),
|
||||
|
||||
// Do not scan wi-fi
|
||||
("geo.wifi.scan", Pref::new(false)),
|
||||
|
||||
// No hang monitor
|
||||
("hangmonitor.timeout", Pref::new(0)),
|
||||
|
||||
// Disable idle-daily notifications to avoid expensive operations
|
||||
// that may cause unexpected test timeouts.
|
||||
("idle.lastDailyNotification", Pref::new(-1)),
|
||||
|
||||
// Disable download and usage of OpenH264, and Widevine plugins
|
||||
("media.gmp-manager.updateEnabled", Pref::new(false)),
|
||||
|
||||
// Disable the GFX sanity window
|
||||
("media.sanity-test.disabled", Pref::new(true)),
|
||||
|
||||
// Do not automatically switch between offline and online
|
||||
("network.manage-offline-status", Pref::new(false)),
|
||||
|
||||
// Make sure SNTP requests do not hit the network
|
||||
("network.sntp.pools", Pref::new("%(server)s")),
|
||||
|
||||
// Disable Flash. The plugin container it is run in is
|
||||
// causing problems when quitting Firefox from geckodriver,
|
||||
// c.f. https://github.com/mozilla/geckodriver/issues/225.
|
||||
("plugin.state.flash", Pref::new(0)),
|
||||
|
||||
// Don't do network connections for mitm priming
|
||||
("security.certerrors.mitm.priming.enabled", Pref::new(false)),
|
||||
|
||||
// Ensure blocklist updates don't hit the network
|
||||
("services.settings.server", Pref::new("")),
|
||||
|
||||
// Disable first run pages
|
||||
("startup.homepage_welcome_url", Pref::new("about:blank")),
|
||||
("startup.homepage_welcome_url.additional", Pref::new("")),
|
||||
|
||||
// asrouter expects a plain object or null
|
||||
("browser.newtabpage.activity-stream.asrouter.providers.cfr", Pref::new("null")),
|
||||
// TODO: Remove once minimum supported Firefox release is 93.
|
||||
("browser.newtabpage.activity-stream.asrouter.providers.cfr-fxa", Pref::new("null")),
|
||||
("browser.newtabpage.activity-stream.asrouter.providers.snippets", Pref::new("null")),
|
||||
("browser.newtabpage.activity-stream.asrouter.providers.message-groups", Pref::new("null")),
|
||||
("browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel", Pref::new("null")),
|
||||
("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", Pref::new("null")),
|
||||
("browser.newtabpage.activity-stream.feeds.system.topstories", Pref::new(false)),
|
||||
("browser.newtabpage.activity-stream.feeds.snippets", Pref::new(false)),
|
||||
("browser.newtabpage.activity-stream.tippyTop.service.endpoint", Pref::new("")),
|
||||
("browser.newtabpage.activity-stream.discoverystream.config", Pref::new("[]")),
|
||||
|
||||
// For Activity Stream firstrun page, use an empty string to avoid fetching.
|
||||
("browser.newtabpage.activity-stream.fxaccounts.endpoint", Pref::new("")),
|
||||
|
||||
// Prevent starting into safe mode after application crashes
|
||||
("toolkit.startup.max_resumed_crashes", Pref::new(-1)),
|
||||
|
||||
// Disable webapp updates.
|
||||
("browser.webapps.checkForUpdates", Pref::new(0)),
|
||||
];
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pub fn assert_de<T>(data: &T, json: serde_json::Value)
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
T: std::cmp::PartialEq,
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
|
||||
json=$(curl -s https://api.github.com/repos/mozilla/geckodriver/releases/latest)
|
||||
url=$(echo "$json" | jq -r '.assets[].browser_download_url | select(contains("linux64") and endswith("gz"))')
|
||||
curl -s -L "$url" | tar -xz
|
||||
chmod +x geckodriver
|
||||
sudo mv geckodriver "$INSTALL_DIR"
|
||||
export PATH=$PATH:$INSTALL_DIR/geckodriver
|
||||
echo "installed geckodriver binary in $INSTALL_DIR"
|
||||
@ -1 +0,0 @@
|
||||
If you use the icons publicly, please link to https://icons8.com/line-awesome somewhere on your page or artwork, so that more creators could know about it and use it for free.
|
||||
@ -1,159 +0,0 @@
|
||||
body {
|
||||
background-color: #f5f5f5;
|
||||
background-image: url('/static/img/bg.png');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.centered {
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
}
|
||||
|
||||
#disk {
|
||||
width: 66vw;
|
||||
border-radius: 1200px;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
background-image: url('/static/img/vinyl.png');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 0 36px 0px rgba(128, 128, 128, 0.128);
|
||||
background-color: white;
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.navbar-avatar {
|
||||
max-height: 40px;
|
||||
border-radius: 30px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.main-menu-link {
|
||||
/*font-size: 24px;*/
|
||||
/*font-family: RobotoSlab;*/
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
#header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#action_list {
|
||||
float: right;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#action_list a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#search_bar {
|
||||
background: url('../svg/search-solid.svg') no-repeat scroll 12px 7px;
|
||||
padding-left: 48px;
|
||||
background-size: 20px;
|
||||
border-radius: 66px;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
/*padding-top: 32px;*/
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
#navigation_logo {
|
||||
max-width: 216px;
|
||||
}
|
||||
|
||||
#content {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#footer {
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dl_queue_img {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icn-spinner {
|
||||
animation: spin-animation 0.9s infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icn-downloading {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.vinyl-card {
|
||||
background-image: url('/static/img/vinyl-card.png');
|
||||
background-position: right;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin-animation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin{
|
||||
from{transform:rotate(0deg)}
|
||||
to{transform:rotate(360deg)}
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
from {-webkit-transform:rotate(0deg);}
|
||||
to { -webkit-transform:rotate(360deg);}
|
||||
}
|
||||
|
||||
@-moz-keyframes spin {
|
||||
from {-moz-transform:rotate(0deg);}
|
||||
to { -moz-transform:rotate(360deg);}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 923 B |
|
Before Width: | Height: | Size: 906 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 902 KiB |
|
Before Width: | Height: | Size: 525 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 136 KiB |
@ -1,60 +0,0 @@
|
||||
const appModal = $('#modalDownloadQueue');
|
||||
const appModalContent = $('#modal_content');
|
||||
let modalPolling = false;
|
||||
|
||||
function proc_notification(icon, title, text) {
|
||||
Swal.fire({
|
||||
title: title,
|
||||
icon: icon,
|
||||
text: text
|
||||
})
|
||||
}
|
||||
|
||||
function fill_download_queue() {
|
||||
$.ajax({
|
||||
url: '/api/v1/get/queue'
|
||||
}).done((res) => {
|
||||
appModalContent.html(res);
|
||||
})
|
||||
}
|
||||
|
||||
$('.settings_btn').on('click', () => {
|
||||
$('#modalSettings').modal('toggle');
|
||||
})
|
||||
|
||||
$('.queue_btn').on('click', () => {
|
||||
console.log('Get Queue!');
|
||||
if (modalPolling) {
|
||||
clearInterval(modalPolling);
|
||||
}
|
||||
fill_download_queue();
|
||||
modalPolling = setInterval(fill_download_queue, 12000);
|
||||
appModal.modal('toggle');
|
||||
})
|
||||
|
||||
$('#download_btn').on('click', () => {
|
||||
let artist = $('#search_bar').val();
|
||||
// Prevent
|
||||
$('#search_bar').val('');
|
||||
let icon = 'error';
|
||||
let title = 'What the flip?!';
|
||||
let text = 'You need to add an artist bro..';
|
||||
|
||||
if (artist) {
|
||||
$("#loader-wrapper").fadeIn(300);
|
||||
$.ajax({
|
||||
url: `/api/v1/get/artist/${artist}`,
|
||||
}).done(function (res) {
|
||||
text = res.message;
|
||||
if (res.status === 200) {
|
||||
icon = 'success';
|
||||
title = 'Shazam!';
|
||||
}
|
||||
$("#loader-wrapper").fadeOut(700);
|
||||
proc_notification(icon, title, text);
|
||||
});
|
||||
} else {
|
||||
proc_notification(icon, title, text);
|
||||
}
|
||||
|
||||
})
|
||||
@ -1,21 +0,0 @@
|
||||
// Bordered & Pulled
|
||||
// -------------------------
|
||||
|
||||
.#{$la-css-prefix}-border {
|
||||
border: solid 0.08em #eee;
|
||||
border-radius: .1em;
|
||||
padding: .2em .25em .15em;
|
||||
}
|
||||
|
||||
.#{$la-css-prefix}-pull-left { float: left; }
|
||||
.#{$la-css-prefix}-pull-right { float: right; }
|
||||
|
||||
.#{$la-css-prefix} {
|
||||
&.#{$la-css-prefix}-pull-left { margin-right: .3em; }
|
||||
&.#{$la-css-prefix}-pull-right { margin-left: .3em; }
|
||||
}
|
||||
|
||||
.#{$la-css-prefix} {
|
||||
&.pull-left { margin-right: .3em; }
|
||||
&.pull-right { margin-left: .3em; }
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
.lar,
|
||||
.las,
|
||||
.lab {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
.#{$la-css-prefix}-fw {
|
||||
width: 1.25em;
|
||||
text-align: center;
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
.#{$la-css-prefix}-lg {
|
||||
font-size: 1.33333em;
|
||||
line-height: 0.75em;
|
||||
vertical-align: -.0667em;
|
||||
}
|
||||
|
||||
.#{$la-css-prefix}-xs { font-size: 0.75em; }
|
||||
.#{$la-css-prefix}-2x { font-size: 1em; }
|
||||
.#{$la-css-prefix}-2x { font-size: 2em; }
|
||||
.#{$la-css-prefix}-3x { font-size: 3em; }
|
||||
.#{$la-css-prefix}-4x { font-size: 4em; }
|
||||
.#{$la-css-prefix}-5x { font-size: 5em; }
|
||||
.#{$la-css-prefix}-6x { font-size: 6em; }
|
||||
.#{$la-css-prefix}-7x { font-size: 7em; }
|
||||
.#{$la-css-prefix}-8x { font-size: 8em; }
|
||||
.#{$la-css-prefix}-9x { font-size: 9em; }
|
||||
.#{$la-css-prefix}-10x { font-size: 10em; }
|
||||
|
||||
.#{$la-css-prefix}-fw {
|
||||
text-align: center;
|
||||
width: 1.25em;
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
.#{$la-css-prefix}-ul {
|
||||
padding-left: 0;
|
||||
margin-left: $la-li-width;
|
||||
list-style-type: none;
|
||||
> li {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$la-css-prefix}-li {
|
||||
position: absolute;
|
||||
left: -2em;
|
||||
text-align: center;
|
||||
width: $la-li-width;
|
||||
line-height: inherit;
|
||||
&.#{$la-css-prefix}-lg {
|
||||
left: -$la-li-width + (4em / 14);
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
// Only display content to screen readers. A la Bootstrap 4.
|
||||
//
|
||||
// See: http://a11yproject.com/posts/how-to-hide-content/
|
||||
|
||||
@mixin sr-only {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
// Use in conjunction with .sr-only to only display content when it's focused.
|
||||
//
|
||||
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
|
||||
//
|
||||
// Credit: HTML5 Boilerplate
|
||||
|
||||
@mixin sr-only-focusable {
|
||||
&:active,
|
||||
&:focus {
|
||||
clip: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
@font-face {
|
||||
font-family: $la-font-name-lab;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-display: auto;
|
||||
src: url('#{$la-font-path}/la-brands-400.eot');
|
||||
src: url("#{$la-font-path}/la-brands-400.eot?#iefix") format("embedded-opentype"),
|
||||
url("#{$la-font-path}/la-brands-400.woff2") format("woff2"),
|
||||
url("#{$la-font-path}/la-brands-400.woff") format("woff"),
|
||||
url("#{$la-font-path}/la-brands-400.ttf") format("truetype"),
|
||||
url("#{$la-font-path}/la-brands-400.svg#lineawesome") format("svg");
|
||||
}
|
||||
|
||||
.#{$la-css-prefix-lab} {
|
||||
font-family: $la-font-name-lab;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: $la-font-name-lar;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: auto;
|
||||
src: url('#{$la-font-path}/la-regular-400.eot');
|
||||
src: url("#{$la-font-path}/la-regular-400.eot?#iefix") format("embedded-opentype"),
|
||||
url("#{$la-font-path}/la-regular-400.woff2") format("woff2"),
|
||||
url("#{$la-font-path}/la-regular-400.woff") format("woff"),
|
||||
url("#{$la-font-path}/la-regular-400.ttf") format("truetype"),
|
||||
url("#{$la-font-path}/la-regular-400.svg#lineawesome") format("svg");
|
||||
}
|
||||
|
||||
.#{$la-css-prefix-lar} {
|
||||
font-family: $la-font-name-lar;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: $la-font-name-las;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: auto;
|
||||
src: url('#{$la-font-path}/la-solid-900.eot');
|
||||
src: url("#{$la-font-path}/la-solid-900.eot?#iefix") format("embedded-opentype"),
|
||||
url("#{$la-font-path}/la-solid-900.woff2") format("woff2"),
|
||||
url("#{$la-font-path}/la-solid-900.woff") format("woff"),
|
||||
url("#{$la-font-path}/la-solid-900.ttf") format("truetype"),
|
||||
url("#{$la-font-path}/la-solid-900.svg#lineawesome") format("svg");
|
||||
}
|
||||
|
||||
.#{$la-css-prefix-las} {
|
||||
font-family: $la-font-name-las;
|
||||
font-weight: 900;
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
.la-pull-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.la-pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.la.la-pull-left,
|
||||
.las.la-pull-left,
|
||||
.lar.la-pull-left,
|
||||
.lal.la-pull-left,
|
||||
.lab.la-pull-left {
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
.la.la-pull-right,
|
||||
.las.la-pull-right,
|
||||
.lar.la-pull-right,
|
||||
.lal.la-pull-right,
|
||||
.lab.la-pull-right {
|
||||
margin-left: .3em;
|
||||
}
|
||||
|
||||
.la-spin {
|
||||
-webkit-animation: la-spin 2s infinite linear;
|
||||
animation: la-spin 2s infinite linear;
|
||||
}
|
||||
|
||||
.la-pulse {
|
||||
-webkit-animation: la-spin 1s infinite steps(8);
|
||||
animation: la-spin 1s infinite steps(8);
|
||||
}
|
||||
|
||||
@-webkit-keyframes la-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes la-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.la-rotate-90 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.la-rotate-180 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.la-rotate-270 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.la-flip-horizontal {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
|
||||
-webkit-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
.la-flip-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
|
||||
-webkit-transform: scale(1, -1);
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
|
||||
.la-flip-both, .la-flip-horizontal.la-flip-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
|
||||
-webkit-transform: scale(-1, -1);
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
|
||||
:root .la-rotate-90,
|
||||
:root .la-rotate-180,
|
||||
:root .la-rotate-270,
|
||||
:root .la-flip-horizontal,
|
||||
:root .la-flip-vertical,
|
||||
:root .la-flip-both {
|
||||
-webkit-filter: none;
|
||||
filter: none;
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
.sr-only { @include sr-only(); }
|
||||
.sr-only-focusable { @include sr-only-focusable(); }
|
||||
@ -1,28 +0,0 @@
|
||||
.#{$la-css-prefix}-stack {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
width: 2.5em;
|
||||
}
|
||||
|
||||
.#{$la-css-prefix}-stack-1x,
|
||||
.#{$la-css-prefix}-stack-2x {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.#{$la-css-prefix}-stack-1x {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.#{$la-css-prefix}-stack-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.#{$la-css-prefix}-inverse {
|
||||
color: $la-inverse;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
@import "mixins";
|
||||
@import "core";
|
||||
@import "variables";
|
||||
@import "path";
|
||||
@import "larger";
|
||||
@import "fixed-width";
|
||||
@import "list";
|
||||
@import "bordered_pulled";
|
||||
@import "rotated-flipped";
|
||||
@import "stacked";
|
||||
@import "icons";
|
||||
@import "screen-reader";
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |