mirror of
https://github.com/MontFerret/ferret.git
synced 2024-12-16 11:37:36 +02:00
sync with MontFerret/ferret
This commit is contained in:
parent
9f7aa5132d
commit
9655efe874
21
.codecov.yml
Normal file
21
.codecov.yml
Normal file
@ -0,0 +1,21 @@
|
||||
ignore:
|
||||
# Ignore coverage for generated code.
|
||||
- "pkg/parser"
|
||||
|
||||
coverage:
|
||||
range: 70..100
|
||||
round: nearest
|
||||
precision: 1
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
enabled: yes
|
||||
threshold: 2%
|
||||
patch: no
|
||||
changes: no
|
||||
|
||||
comment:
|
||||
layout: "header, diff"
|
||||
behavior: once
|
||||
require_changes: yes
|
@ -12,9 +12,7 @@ builds:
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
|
||||
archive:
|
||||
|
26
.travis.yml
26
.travis.yml
@ -6,9 +6,9 @@ os:
|
||||
- linux
|
||||
|
||||
go:
|
||||
- "1.10"
|
||||
- "1.11"
|
||||
- master
|
||||
- "1.11.x"
|
||||
- "1.12.x"
|
||||
- stable
|
||||
|
||||
addons:
|
||||
apt:
|
||||
@ -16,19 +16,16 @@ addons:
|
||||
- oracle-java8-set-default
|
||||
chrome: stable
|
||||
|
||||
before_install:
|
||||
- curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
install:
|
||||
- go get -u github.com/mgechev/revive
|
||||
- sudo curl -o /usr/local/lib/antlr-4.7.1-complete.jar https://www.antlr.org/download/antlr-4.7.1-complete.jar
|
||||
- export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH"
|
||||
- mkdir $HOME/travis-bin
|
||||
- echo -e '#!/bin/bash\njava -jar /usr/local/lib/antlr-4.7.1-complete.jar "$@"' > $HOME/travis-bin/antlr4
|
||||
- echo -e '#!/bin/bash\njava -jar /usr/local/lib/antlr-4.7.1-complete.jar "$@"' > $HOME/travis-bin/antlr
|
||||
- echo -e '#!/bin/bash\njava org.antlr.v4.gui.TestRig "$@"' > $HOME/travis-bin/grun
|
||||
- chmod +x $HOME/travis-bin/*
|
||||
- export PATH=$PATH:$HOME/travis-bin
|
||||
|
||||
install:
|
||||
- make install
|
||||
- export GO111MODULE=on
|
||||
|
||||
stages:
|
||||
- lint
|
||||
@ -40,19 +37,23 @@ stages:
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
go: stable
|
||||
script:
|
||||
- make vet
|
||||
- make lint
|
||||
- make fmt
|
||||
- git diff
|
||||
- if [[ $(git diff) != '' ]]; then echo 'Invalid formatting!' >&2; exit 1; fi
|
||||
- stage: compile
|
||||
go: stable
|
||||
script:
|
||||
- make generate
|
||||
- make compile
|
||||
- stage: test
|
||||
script:
|
||||
- make cover
|
||||
after_success:
|
||||
- if [ $TRAVIS_BRANCH == "master" && $TRAVIS_PULL_REQUEST == "false" ]; then bash <(curl -s https://codecov.io/bash); fi
|
||||
- stage: e2e
|
||||
go: stable
|
||||
before_script:
|
||||
- google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 --disable-setuid-sandbox --no-sandbox about:blank &
|
||||
script:
|
||||
@ -60,5 +61,6 @@ jobs:
|
||||
after_script:
|
||||
- killall google-chrome-stable
|
||||
- stage: bench
|
||||
go: stable
|
||||
script:
|
||||
- make bench
|
||||
- make bench
|
||||
|
120
CHANGELOG.md
120
CHANGELOG.md
@ -1,5 +1,125 @@
|
||||
## Changelog
|
||||
|
||||
### 0.7.0
|
||||
#### Added
|
||||
- Autocomplete to CLI [#219](https://github.com/MontFerret/ferret/pull/219).
|
||||
- New mouse functions - ``MOUSE(x, y)`` and ``SCROLL(x, y)`` [#237](https://github.com/MontFerret/ferret/pull/237).
|
||||
- ``WAIT_NO_ELEMENT``, ``WAIT_NO_CLASS`` and ``WAIT_NO_CLASS_ALL`` functions [#249](https://github.com/MontFerret/ferret/pull/249).
|
||||
- Computed ``HTMLElement.style`` property [#255](https://github.com/MontFerret/ferret/pull/255).
|
||||
- ``ATTR_GET``, ``ATTR_SET``, ``ATTR_REMOVE``, ``STYLE_GET``, ``STYLE_SET`` and ``STYLE_REMOVE`` functions [#255](https://github.com/MontFerret/ferret/pull/255).
|
||||
- ``WAIT_STYLE``, ``WAIT_NO_STYLE``, ``WAIT_STYLE_ALL`` and ``WAIT_NO_STYLE_ALL`` functions [#256](https://github.com/MontFerret/ferret/pull/260).
|
||||
- Cookies support. Now a document can be loaded with preset cookies. Also, HTMLDocument has ``.cookies`` property.
|
||||
In order to manipulate with cookies, ``COOKIE_DEL``, ``COOKIE_SET`` AND ``COOKIE_GET`` functions were added [#242](https://github.com/MontFerret/ferret/pull/242).
|
||||
|
||||
```
|
||||
LET doc = DOCUMENT(url, {
|
||||
driver: "cdp",
|
||||
cookies: [{
|
||||
name: "x-e2e",
|
||||
value: "test"
|
||||
}, {
|
||||
name: "x-e2e-2",
|
||||
value: "test2"
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
#### Changed
|
||||
- Renamed ParseTYPEP to MustParseTYPE [#231](https://github.com/MontFerret/ferret/pull/231).
|
||||
- Added context to all HTML object [#235](https://github.com/MontFerret/ferret/pull/235).
|
||||
|
||||
#### Fixed
|
||||
- Click events are not cancellable [#222](https://github.com/MontFerret/ferret/pull/222).
|
||||
- Name collision [#223](https://github.com/MontFerret/ferret/pull/223).
|
||||
- Invalid return in FQL Compiler constructor [#227](https://github.com/MontFerret/ferret/pull/227).
|
||||
- Incorrect string length computation [#238](https://github.com/MontFerret/ferret/pull/238).
|
||||
- Access to HTML object properties via dot notation [#239](https://github.com/MontFerret/ferret/pull/239).
|
||||
- Graceful process termination [#240](https://github.com/MontFerret/ferret/pull/240).
|
||||
- Browser launcher for macOS [#246](https://github.com/MontFerret/ferret/pull/246).
|
||||
|
||||
#### Breaking changes
|
||||
- New runtime type system [#232](https://github.com/MontFerret/ferret/pull/232).
|
||||
- Moved and renamed ``collections.IterableCollection`` and ```collections.CollectionIterator``` interfaces.
|
||||
Now they are in ``core`` package and called ``Iterable`` and ``Iterator`` [1af8b37](https://github.com/MontFerret/ferret/commit/f8e061cc8034fd4cfa4ce2a094276d50137a4b98).
|
||||
- Renamed ``collections.Collection`` interface to ``collections.Measurable`` [1af8b37](https://github.com/MontFerret/ferret/commit/f8e061cc8034fd4cfa4ce2a094276d50137a4b98).
|
||||
- Moved html interfaces from ``runtime/values`` package into ``drivers`` package [#234](https://github.com/MontFerret/ferret/pull/234).
|
||||
- Changed drivers initialization. Replaced old ``drivers.WithDynamic`` and ``drivers.WithStatic`` methods with a new ``drivers.WithContext`` method with optional parameter ``drivers.AsDefault()`` [#234](https://github.com/MontFerret/ferret/pull/234).
|
||||
- New document load params [#234](https://github.com/MontFerret/ferret/pull/234).
|
||||
```
|
||||
LET doc = DOCUMENT(url, {
|
||||
driver: "cdp"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### 0.6.0
|
||||
#### Added
|
||||
- Added support for ```context.Done()``` to interrupt an execution [#201](https://github.com/MontFerret/ferret/pull/201).
|
||||
- Added support for custom HTML drivers [#209](https://github.com/MontFerret/ferret/pull/209).
|
||||
- Added support for dot notation access and assignments for custom types [#214](https://github.com/MontFerret/ferret/pull/214/commits/0ea36e511540e569ef53b8748301512b6d8a046b)
|
||||
- Added ```ELEMENT_EXISTS(doc, selector) -> Boolean``` function [#210](https://github.com/MontFerret/ferret/pull/210).
|
||||
```
|
||||
LET exists = ELEMENT_EXISTS(doc, ".nav")
|
||||
```
|
||||
- Added ```PageLoadParams``` to ```DOCUMENT``` function [#214](https://github.com/MontFerret/ferret/pull/214/commits/3434323cd08ca3186e90cb5ab1faa26e28a28709).
|
||||
```
|
||||
LET doc = DOCUMENT("https://www.google.com/", {
|
||||
dynamic: true,
|
||||
timeout: 10000
|
||||
})
|
||||
```
|
||||
|
||||
#### Fixed
|
||||
- Math operators precedence [#202](https://github.com/MontFerret/ferret/pull/202).
|
||||
- Memory leak in ```DOWNLOAD``` function [#213](https://github.com/MontFerret/ferret/pull/213).
|
||||
|
||||
#### Breaking change
|
||||
- **(Embedded)** Removed builtin drivers initialization in Program [#198](https://github.com/MontFerret/ferret/pull/198).
|
||||
The initialization must be done via context manually.
|
||||
|
||||
### 0.5.2
|
||||
#### Fixed
|
||||
- Does not close browser tab when fails to load a page [#193](https://github.com/MontFerret/ferret/pull/193).
|
||||
- ```HTMLElement.value``` does not return actual value [#195](https://github.com/MontFerret/ferret/pull/195)
|
||||
- Compiles a query with duplicate variable in FOR statement [#196](https://github.com/MontFerret/ferret/pull/196)
|
||||
- Default CDP address [#197](https://github.com/MontFerret/ferret/pull/197).
|
||||
|
||||
### 0.5.1
|
||||
#### Fixed
|
||||
- Unable to change a page load timeout [#186](https://github.com/MontFerret/ferret/pull/186).
|
||||
- ``RETURN doc`` returns an empty string [#187](https://github.com/MontFerret/ferret/pull/187).
|
||||
- Unable to pass an HTML Node without a selector to ``INNER_TEXT`` and ``INNER_HTML`` [#187](https://github.com/MontFerret/ferret/pull/187).
|
||||
- ``doc.innerText`` returns an error [#187](https://github.com/MontFerret/ferret/pull/187).
|
||||
- Panics when ``WAIT_CLASS`` does not receive all required arguments [#192](https://github.com/MontFerret/ferret/pull/192).
|
||||
|
||||
### 0.5.0
|
||||
#### Added
|
||||
- ``FMT`` function [#151](https://github.com/MontFerret/ferret/pull/151).
|
||||
- DateTime functions [#152](https://github.com/MontFerret/ferret/pull/152), [#153](https://github.com/MontFerret/ferret/pull/153), [#154](https://github.com/MontFerret/ferret/pull/154), [#156](https://github.com/MontFerret/ferret/pull/156), [#157](https://github.com/MontFerret/ferret/pull/157), [#165](https://github.com/MontFerret/ferret/pull/165), [#175](https://github.com/MontFerret/ferret/pull/175), [#182](https://github.com/MontFerret/ferret/pull/182).
|
||||
- ``PAGINATION`` function [#173](https://github.com/MontFerret/ferret/pull/173).
|
||||
- ``SCROLL_TOP``, ``SCROLL_BOTTOM`` and ``SCROLL_ELEMENT`` functions [#174](https://github.com/MontFerret/ferret/pull/174).
|
||||
- ``HOVER`` function [#178](https://github.com/MontFerret/ferret/pull/178).
|
||||
- Panic recovery mechanism [#158](https://github.com/MontFerret/ferret/pull/158).
|
||||
|
||||
#### Fixed
|
||||
- Unable to define variables and make function calls before FILTER, SORT and etc statements [#148](https://github.com/MontFerret/ferret/pull/148).
|
||||
- Unable to use params in LIMIT clause [#173](https://github.com/MontFerret/ferret/pull/173).
|
||||
- ```RIGHT``` should return substr counting from right rather than left [#164](https://github.com/MontFerret/ferret/pull/164).
|
||||
- ``INNER_HTML`` returns outer HTML instead for dynamic elements [#170](https://github.com/MontFerret/ferret/pull/170).
|
||||
- ``INNER_TEXT`` returns HTML instead from dynamic elements [#170](https://github.com/MontFerret/ferret/pull/170).
|
||||
|
||||
#### Breaking change:
|
||||
- Name collision between ```math``` and ```utils``` packages in standard library. Renamed ```LOG``` to ```PRINT``` [#162](https://github.com/MontFerret/ferret/pull/162).
|
||||
|
||||
### 0.4.0
|
||||
#### Added
|
||||
- ``COLLECT`` keyword [#141](https://github.com/MontFerret/ferret/pull/141)
|
||||
- ``VALUES`` function [#128](https://github.com/MontFerret/ferret/pull/128)
|
||||
- ``MERGE_RECURSIVE`` function [#140](https://github.com/MontFerret/ferret/pull/140)
|
||||
|
||||
#### Fixed
|
||||
- Unable to use string literals as object properties [commit](https://github.com/MontFerret/ferret/commit/685c5872aaed42852ce32e7ab8b69b1a269185be)
|
||||
|
||||
### 0.3.0
|
||||
|
||||
#### Added
|
||||
|
311
Gopkg.lock
generated
311
Gopkg.lock
generated
@ -1,311 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a62f6ed230a8cd138a9efbe718e7d0b0294f139266f5f55cd942769a9aac8de2"
|
||||
name = "github.com/PuerkitoBio/goquery"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "dc2ec5c7ca4d9aae063b79b9f581dd3ea6afd2b2"
|
||||
version = "v1.4.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:66b3310cf22cdc96c35ef84ede4f7b9b370971c4025f394c89a2638729653b11"
|
||||
name = "github.com/andybalholm/cascadia"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "901648c87902174f774fac311d7f176f8647bdaa"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5e79f7a65816b2ba311439df21c9b24af91868cbb5d67489be6d94be2cccb286"
|
||||
name = "github.com/antlr/antlr4"
|
||||
packages = ["runtime/Go/antlr"]
|
||||
pruneopts = "UT"
|
||||
revision = "bdc05c87be2ad981744223df0fd745e8345baba9"
|
||||
version = "4.7.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b95738a1e6ace058b5b8544303c0871fc01d224ef0d672f778f696265d0f2917"
|
||||
name = "github.com/chzyer/readline"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "62c6fe6193755f722b8b8788aa7357be55a50ff1"
|
||||
version = "v1.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cc439e1d9d8cff3d575642f5401033b00f2b8d0cd9f859db45604701c990879a"
|
||||
name = "github.com/corpix/uarand"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2b8494104d86337cdd41d0a49cbed8e4583c0ab4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ce579162ae1341f3e5ab30c0dce767f28b1eb6a81359aad01723f1ba6b4becdf"
|
||||
name = "github.com/gofrs/uuid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "370558f003bfe29580cd0f698d8640daccdcc45c"
|
||||
version = "v3.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795"
|
||||
name = "github.com/gopherjs/gopherjs"
|
||||
packages = ["js"]
|
||||
pruneopts = "UT"
|
||||
revision = "0210a2f0f73c96103378b0b935f39868e5731809"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1e15f4e455f94aeaedfcf9c75b3e1c449b5acba1551c58446b4b45be507c707b"
|
||||
name = "github.com/jtolds/gls"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064"
|
||||
version = "v4.2.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:63801c64679bdeef1b63206f065d5a1f4c3a75ed66a3fbbd909bf98766542980"
|
||||
name = "github.com/labstack/echo"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "1abaa3049251d17932e4313c2d6165073fd07fd8"
|
||||
version = "v3.3.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:faee5b9f53eb1ae4eb04708c040c8c4dd685ce46509671e57a08520a15c54368"
|
||||
name = "github.com/labstack/gommon"
|
||||
packages = [
|
||||
"color",
|
||||
"log",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "2a618302b929cc20862dda3aa6f02f64dbe740dd"
|
||||
version = "v0.2.7"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7ffdd69928c5153fc351132aa80bbcc18a8f8122de1ba592cf42dccb65732361"
|
||||
name = "github.com/mafredri/cdp"
|
||||
packages = [
|
||||
".",
|
||||
"devtool",
|
||||
"internal/errors",
|
||||
"protocol",
|
||||
"protocol/accessibility",
|
||||
"protocol/animation",
|
||||
"protocol/applicationcache",
|
||||
"protocol/audits",
|
||||
"protocol/browser",
|
||||
"protocol/cachestorage",
|
||||
"protocol/console",
|
||||
"protocol/css",
|
||||
"protocol/database",
|
||||
"protocol/debugger",
|
||||
"protocol/deviceorientation",
|
||||
"protocol/dom",
|
||||
"protocol/domdebugger",
|
||||
"protocol/domsnapshot",
|
||||
"protocol/domstorage",
|
||||
"protocol/emulation",
|
||||
"protocol/headlessexperimental",
|
||||
"protocol/heapprofiler",
|
||||
"protocol/indexeddb",
|
||||
"protocol/input",
|
||||
"protocol/inspector",
|
||||
"protocol/internal",
|
||||
"protocol/io",
|
||||
"protocol/layertree",
|
||||
"protocol/log",
|
||||
"protocol/memory",
|
||||
"protocol/network",
|
||||
"protocol/overlay",
|
||||
"protocol/page",
|
||||
"protocol/performance",
|
||||
"protocol/profiler",
|
||||
"protocol/runtime",
|
||||
"protocol/schema",
|
||||
"protocol/security",
|
||||
"protocol/serviceworker",
|
||||
"protocol/storage",
|
||||
"protocol/systeminfo",
|
||||
"protocol/target",
|
||||
"protocol/testing",
|
||||
"protocol/tethering",
|
||||
"protocol/tracing",
|
||||
"rpcc",
|
||||
"session",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "75b0ecc5efcff27ac756a33ec71f0db75dc3d21c"
|
||||
version = "v0.19.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
|
||||
version = "v0.0.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c805e517269b0ba4c21ded5836019ed7d16953d4026cb7d00041d039c7906be9"
|
||||
name = "github.com/natefinch/lumberjack"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "a96e63847dc3c67d17befa69c303767e2f84e54f"
|
||||
version = "v2.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5bc8f93f977b72a7a5264725c3bab275e69de0cc3e5c0dc1ee56feb564c33f03"
|
||||
name = "github.com/rs/zerolog"
|
||||
packages = [
|
||||
".",
|
||||
"internal/cbor",
|
||||
"internal/json",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "338f9bc14084d22cb8eeacd6492861f8449d715c"
|
||||
version = "v1.9.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4ca145a665316d3c020a39c0741780fa3636b9152b824206796c4dce541f4a24"
|
||||
name = "github.com/sethgrid/pester"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "03e26c9abbbf5accb8349790bf9f41bde09d72c3"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cc1c574c9cb5e99b123888c12b828e2d19224ab6c2244bda34647f230bf33243"
|
||||
name = "github.com/smartystreets/assertions"
|
||||
packages = [
|
||||
".",
|
||||
"internal/go-render/render",
|
||||
"internal/oglematchers",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7678a5452ebea5b7090a6b163f844c133f523da2"
|
||||
version = "1.8.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a3e081e593ee8e3b0a9af6a5dcac964c67a40c4f2034b5345b2ad78d05920728"
|
||||
name = "github.com/smartystreets/goconvey"
|
||||
packages = [
|
||||
"convey",
|
||||
"convey/gotest",
|
||||
"convey/reporting",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
|
||||
version = "1.6.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c468422f334a6b46a19448ad59aaffdfc0a36b08fdcc1c749a0b29b6453d7e59"
|
||||
name = "github.com/valyala/bytebufferpool"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:268b8bce0064e8c057d7b913605459f9a26dcab864c0886a56d196540fbf003f"
|
||||
name = "github.com/valyala/fasttemplate"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:dedf20eb0d3e8d6aa8a4d3d2fae248222b688ed528201995e152cc497899123c"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"acme",
|
||||
"acme/autocert",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "a92615f3c49003920a58dedcf32cf55022cefb8d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:24641a15105cc4281b8a4a701ac4019194618102c327b1495485b3f19ffdd53e"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"html",
|
||||
"html/atom",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "26e67e76b6c3f6ce91f7c52def5af501b4e0f3a2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
pruneopts = "UT"
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0a40b0bdd57a93e741d8557465be3a2edeec408e9b6399586ad65bbe8e355796"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
pruneopts = "UT"
|
||||
revision = "fa43e7bc11baaae89f3f902b2b4d832b68234844"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/PuerkitoBio/goquery",
|
||||
"github.com/antlr/antlr4/runtime/Go/antlr",
|
||||
"github.com/chzyer/readline",
|
||||
"github.com/corpix/uarand",
|
||||
"github.com/gofrs/uuid",
|
||||
"github.com/labstack/echo",
|
||||
"github.com/mafredri/cdp",
|
||||
"github.com/mafredri/cdp/devtool",
|
||||
"github.com/mafredri/cdp/protocol/dom",
|
||||
"github.com/mafredri/cdp/protocol/emulation",
|
||||
"github.com/mafredri/cdp/protocol/input",
|
||||
"github.com/mafredri/cdp/protocol/page",
|
||||
"github.com/mafredri/cdp/protocol/runtime",
|
||||
"github.com/mafredri/cdp/protocol/target",
|
||||
"github.com/mafredri/cdp/rpcc",
|
||||
"github.com/mafredri/cdp/session",
|
||||
"github.com/natefinch/lumberjack",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/rs/zerolog",
|
||||
"github.com/sethgrid/pester",
|
||||
"github.com/smartystreets/goconvey/convey",
|
||||
"golang.org/x/net/html",
|
||||
"golang.org/x/sync/errgroup",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
50
Gopkg.toml
50
Gopkg.toml
@ -1,50 +0,0 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/antlr/antlr4"
|
||||
version = "4.7.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mafredri/cdp"
|
||||
version = "0.19.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/chzyer/readline"
|
||||
version = "1.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/PuerkitoBio/goquery"
|
||||
version = "1.4.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gofrs/uuid"
|
||||
version = "3.1.1"
|
26
Makefile
26
Makefile
@ -1,8 +1,9 @@
|
||||
.PHONY: build compile install test e2e doc fmt lint vet release
|
||||
.PHONY: build compile test e2e doc fmt lint vet release
|
||||
|
||||
export GOPATH
|
||||
|
||||
VERSION ?= $(shell git describe --tags --always --dirty)
|
||||
RELEASE_VERSION ?= $(version)
|
||||
DIR_BIN = ./bin
|
||||
DIR_PKG = ./pkg
|
||||
DIR_CLI = ./cli
|
||||
@ -10,24 +11,22 @@ DIR_E2E = ./e2e
|
||||
|
||||
default: build
|
||||
|
||||
build: install vet generate test compile
|
||||
build: vet generate test compile
|
||||
|
||||
compile:
|
||||
go build -v -o ${DIR_BIN}/ferret \
|
||||
-ldflags "-X main.version=${VERSION}" \
|
||||
./main.go
|
||||
|
||||
install:
|
||||
dep ensure
|
||||
|
||||
test:
|
||||
go test -v -race ${DIR_PKG}/...
|
||||
go test -race -v ${DIR_PKG}/...
|
||||
|
||||
cover:
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic ${DIR_PKG}/...
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic ${DIR_PKG}/... && \
|
||||
curl -s https://codecov.io/bash | bash
|
||||
|
||||
e2e:
|
||||
go run ${DIR_E2E}/main.go --tests ${DIR_E2E}/tests --pages ${DIR_E2E}/pages
|
||||
go run ${DIR_E2E}/main.go --tests ${DIR_E2E}/tests --pages ${DIR_E2E}/pages --filter doc_cookie_set*
|
||||
|
||||
bench:
|
||||
go test -run=XXX -bench=. ${DIR_PKG}/...
|
||||
@ -53,4 +52,13 @@ vet:
|
||||
go vet ${DIR_CLI}/... ${DIR_PKG}/...
|
||||
|
||||
release:
|
||||
goreleaser
|
||||
ifeq ($(RELEASE_VERSION), )
|
||||
$(error "Release version is required (version=x)")
|
||||
else ifeq ($(GITHUB_TOKEN), )
|
||||
$(error "GitHub token is required (GITHUB_TOKEN)")
|
||||
else
|
||||
rm -rf ./dist && \
|
||||
git tag -a v$(RELEASE_VERSION) -m "New $(RELEASE_VERSION) version" && \
|
||||
git push origin v$(RELEASE_VERSION) && \
|
||||
goreleaser
|
||||
endif
|
||||
|
203
README.md
203
README.md
@ -1,24 +1,38 @@
|
||||
# Ferret
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.com/MontFerret/ferret"><img alt="Build Status" src="https://travis-ci.com/MontFerret/ferret.svg?branch=master"></a>
|
||||
<a href="https://codecov.io/gh/MontFerret/ferret">
|
||||
<img src="https://codecov.io/gh/MontFerret/ferret/branch/master/graph/badge.svg" />
|
||||
</a>
|
||||
<a href="https://discord.gg/kzet32U"><img alt="Discord Chat" src="https://img.shields.io/discord/501533080880676864.svg"></a>
|
||||
<a href="http://opensource.org/licenses/MIT"><img alt="MIT License" src="http://img.shields.io/badge/license-MIT-brightgreen.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/MontFerret/ferret">
|
||||
<img alt="Go Report Status" src="https://goreportcard.com/badge/github.com/MontFerret/ferret">
|
||||
</a>
|
||||
<a href="https://travis-ci.com/MontFerret/ferret">
|
||||
<img alt="Build Status" src="https://travis-ci.com/MontFerret/ferret.svg?branch=master">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/MontFerret/ferret">
|
||||
<img src="https://codecov.io/gh/MontFerret/ferret/branch/master/graph/badge.svg" />
|
||||
</a>
|
||||
<a href="https://discord.gg/kzet32U">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/501533080880676864.svg">
|
||||
</a>
|
||||
<a href="https://github.com/MontFerret/ferret/releases">
|
||||
<img alt="Ferret release" src="https://img.shields.io/github/release/MontFerret/ferret.svg">
|
||||
</a>
|
||||
<a href="http://opensource.org/licenses/MIT">
|
||||
<img alt="MIT License" src="http://img.shields.io/badge/license-MIT-brightgreen.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
![ferret](https://raw.githubusercontent.com/MontFerret/ferret/master/assets/intro.jpg)
|
||||
|
||||
## What is it?
|
||||
```ferret``` is a web scraping system aiming to simplify data extraction from the web for such things like UI testing, machine learning and analytics.
|
||||
Having its own declarative language, ```ferret``` abstracts away technical details and complexity of the underlying technologies, helping to focus on the data itself.
|
||||
It's extremely portable, extensible and fast.
|
||||
```ferret``` is a web scraping system. It aims to simplify data extraction from the web for UI testing, machine learning, analytics and more.
|
||||
```ferret``` allows users to focus on the data. It abstracts away the technical details and complexity of underlying technologies using its own declarative language.
|
||||
It is extremely portable, extensible and fast.
|
||||
|
||||
[Read the introductory blog post about Ferret here!](https://medium.com/@ziflex/say-hello-to-ferret-a-modern-web-scraping-tool-5c9cc85ba183)
|
||||
|
||||
## Show me some code
|
||||
The following example demonstrates the use of dynamic pages.
|
||||
First of all, we load the main Google Search page, type search criteria into an input box and then click a search button.
|
||||
The click action triggers a redirect, so we wait till its end.
|
||||
We load the main Google Search page, type search criteria into an input box and then click a search button.
|
||||
The click action triggers a redirect, so we wait until its end.
|
||||
Once the page gets loaded, we iterate over all elements in search results and assign the output to a variable.
|
||||
The final for loop filters out empty elements that might be because of inaccurate use of selectors.
|
||||
|
||||
@ -71,11 +85,10 @@ You can download latest binaries from [here](https://github.com/MontFerret/ferre
|
||||
|
||||
### Source code
|
||||
#### Production
|
||||
* Go >=1.10
|
||||
* Go >=1.11
|
||||
* Chrome or Docker
|
||||
|
||||
#### Development
|
||||
* GoDep
|
||||
* GNU Make
|
||||
* ANTLR4 >=4.7.1
|
||||
|
||||
@ -90,8 +103,8 @@ In order to use all Ferret features, you will need to have Chrome either install
|
||||
For ease of use we recommend to run Chrome inside a Docker container:
|
||||
|
||||
```sh
|
||||
docker pull alpeware/chrome-headless-trunk
|
||||
docker run -d -p=0.0.0.0:9222:9222 --name=chrome-headless -v /tmp/chromedata/:/data alpeware/chrome-headless-trunk
|
||||
docker pull alpeware/chrome-headless-stable
|
||||
docker run -d -p=0.0.0.0:9222:9222 --name=chrome-headless -v /tmp/chromedata/:/data alpeware/chrome-headless-stable
|
||||
```
|
||||
|
||||
But if you want to see what's happening during query execution, just start your Chrome with remote debugging port:
|
||||
@ -207,14 +220,18 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/MontFerret/ferret/pkg/compiler"
|
||||
"os"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/compiler"
|
||||
"github.com/MontFerret/ferret/pkg/drivers"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/cdp"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/http"
|
||||
)
|
||||
|
||||
type Topic struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Url string `json:"url"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@ -226,7 +243,7 @@ func main() {
|
||||
}
|
||||
|
||||
for _, topic := range topics {
|
||||
fmt.Println(fmt.Sprintf("%s: %s %s", topic.Name, topic.Description, topic.Url))
|
||||
fmt.Println(fmt.Sprintf("%s: %s %s", topic.Name, topic.Description, topic.URL))
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +272,17 @@ func getTopTenTrendingTopics() ([]*Topic, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, err := program.Run(context.Background())
|
||||
// create a root context
|
||||
ctx := context.Background()
|
||||
|
||||
// enable HTML drivers
|
||||
// by default, Ferret Runtime does not know about any HTML drivers
|
||||
// all HTML manipulations are done via functions from standard library
|
||||
// that assume that at least one driver is available
|
||||
ctx = drivers.WithContext(ctx, cdp.NewDriver())
|
||||
ctx = drivers.WithContext(ctx, http.NewDriver(), drivers.AsDefault())
|
||||
|
||||
out, err := program.Run(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -271,6 +298,7 @@ func getTopTenTrendingTopics() ([]*Topic, error) {
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Extensibility
|
||||
@ -284,10 +312,12 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/compiler"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -307,7 +337,7 @@ func getStrings() ([]string, error) {
|
||||
// function implements is a type of a function that ferret supports as a runtime function
|
||||
transform := func(ctx context.Context, args ...core.Value) (core.Value, error) {
|
||||
// it's just a helper function which helps to validate a number of passed args
|
||||
err := core.ValidateArgs(args, 1)
|
||||
err := core.ValidateArgs(args, 1, 1)
|
||||
|
||||
if err != nil {
|
||||
// it's recommended to return built-in None type, instead of nil
|
||||
@ -324,7 +354,7 @@ func getStrings() ([]string, error) {
|
||||
// cast to built-in string type
|
||||
str := args[0].(values.String)
|
||||
|
||||
return str.Concat(values.NewString("_ferret")).ToUpper(), nil
|
||||
return values.NewString(strings.ToUpper(str.String() + "_ferret")), nil
|
||||
}
|
||||
|
||||
query := `
|
||||
@ -334,7 +364,10 @@ func getStrings() ([]string, error) {
|
||||
`
|
||||
|
||||
comp := compiler.New()
|
||||
comp.RegisterFunction("transform", transform)
|
||||
|
||||
if err := comp.RegisterFunction("transform", transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
program, err := comp.Compile(query)
|
||||
|
||||
@ -384,3 +417,127 @@ func main() {
|
||||
comp.RegisterFunctions(strings.NewLib())
|
||||
}
|
||||
```
|
||||
|
||||
## Proxy
|
||||
|
||||
By default, Ferret does not use any proxies. Partially, due to inability to force Chrome/Chromium (or any other Chrome Devtools Protocol compatible browser) to use a prticular proxy. It should be done during a browser launch.
|
||||
|
||||
But you can pass an address of a proxy server you want to use for static pages.
|
||||
|
||||
#### CLI
|
||||
|
||||
```sh
|
||||
ferret --proxy=http://localhost:8888 my-query.fql
|
||||
```
|
||||
|
||||
#### Code
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/compiler"
|
||||
"github.com/MontFerret/ferret/pkg/drivers"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/http"
|
||||
)
|
||||
|
||||
func run(q string) ([]byte, error) {
|
||||
proxy := "http://localhost:8888"
|
||||
comp := compiler.New()
|
||||
program := comp.MustCompile(q)
|
||||
|
||||
// create a root context
|
||||
ctx := context.Background()
|
||||
|
||||
// we inform the driver what proxy to use
|
||||
ctx = drivers.WithContext(ctx, http.NewDriver(http.WithProxy(proxy)), drivers.AsDefault())
|
||||
|
||||
return program.Run(ctx)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Cookies
|
||||
|
||||
### Non-incognito mode
|
||||
|
||||
By default, ``CDP`` driver execute each query in an incognito mode in order to avoid any collisions related to some persisted cookies from previous queries.
|
||||
However, sometimes it might not be a desirable behavior and a query needs to be executed within a Chrome tab with earlier persisted cookies.
|
||||
In order to do that, we need to inform the driver to execute all queries in regular tabs. Here is how to do that:
|
||||
|
||||
#### CLI
|
||||
|
||||
```sh
|
||||
ferret --cdp-keep-cookies my-query.fql
|
||||
```
|
||||
|
||||
#### Code
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/compiler"
|
||||
"github.com/MontFerret/ferret/pkg/drivers"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/cdp"
|
||||
)
|
||||
|
||||
func run(q string) ([]byte, error) {
|
||||
comp := compiler.New()
|
||||
program := comp.MustCompile(q)
|
||||
|
||||
// create a root context
|
||||
ctx := context.Background()
|
||||
|
||||
// we inform the driver to keep cookies between queries
|
||||
ctx = drivers.WithContext(
|
||||
ctx,
|
||||
cdp.NewDriver(cdp.WithKeepCookies()),
|
||||
drivers.AsDefault(),
|
||||
)
|
||||
|
||||
return program.Run(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
#### Query
|
||||
```
|
||||
LET doc = DOCUMENT("https://www.google.com", {
|
||||
driver: "cdp",
|
||||
keepCookies: true
|
||||
})
|
||||
```
|
||||
|
||||
### Cookies manipulation
|
||||
For more precise work, you can set/get/delete cookies manually during and after page load:
|
||||
|
||||
```
|
||||
LET doc = DOCUMENT("https://www.google.com", {
|
||||
driver: "cdp",
|
||||
cookies: [
|
||||
{
|
||||
name: "foo",
|
||||
value: "bar"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
COOKIES_SET(doc, { name: "baz", value: "qaz"}, { name: "daz", value: "gag" })
|
||||
COOKIES_DEL(doc, "foo")
|
||||
|
||||
LET c = COOKIES_GET(doc, "baz")
|
||||
|
||||
FOR cookie IN doc.cookies
|
||||
RETURN cookie.name
|
||||
|
||||
```
|
||||
|
50
cli/autocompleter.go
Normal file
50
cli/autocompleter.go
Normal file
@ -0,0 +1,50 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/derekparker/trie"
|
||||
)
|
||||
|
||||
// AutoCompleter autocompletes queries
|
||||
// into the REPL.
|
||||
// Implements AutoCompleter interface from
|
||||
// github.com/chzyer/readline
|
||||
type AutoCompleter struct {
|
||||
coreFuncs *trie.Trie
|
||||
}
|
||||
|
||||
func NewAutoCompleter(functions []string) *AutoCompleter {
|
||||
coreFuncs := trie.New()
|
||||
|
||||
for _, function := range functions {
|
||||
coreFuncs.Add(function, function)
|
||||
}
|
||||
|
||||
return &AutoCompleter{
|
||||
coreFuncs: coreFuncs,
|
||||
}
|
||||
}
|
||||
|
||||
// Do implements method of AutoCompleter interface
|
||||
func (ac *AutoCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
|
||||
lineStr := string(line)
|
||||
tokens := strings.Split(lineStr, " ")
|
||||
token := tokens[len(tokens)-1]
|
||||
|
||||
// if remove this check, than
|
||||
// on any empty string will return
|
||||
// all available functions
|
||||
if token == "" {
|
||||
return newLine, pos
|
||||
}
|
||||
|
||||
for _, fn := range ac.coreFuncs.PrefixSearch(token) {
|
||||
// cuts a piece of word that is already written
|
||||
// in the repl
|
||||
withoutPre := []rune(fn)[len(token):]
|
||||
newLine = append(newLine, withoutPre)
|
||||
}
|
||||
|
||||
return newLine, pos
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Browser struct {
|
||||
@ -18,7 +19,7 @@ func (b *Browser) Flags() Flags {
|
||||
|
||||
func (b *Browser) DebuggingAddress() string {
|
||||
if !b.Flags().Has("remote-debugging-address") {
|
||||
b.Flags().Set("remote-debugging-address", "0.0.0.0")
|
||||
b.Flags().Set("remote-debugging-address", "http://0.0.0.0:9222")
|
||||
}
|
||||
|
||||
value, _ := b.Flags().Get("remote-debugging-address")
|
||||
@ -39,12 +40,16 @@ func (b *Browser) DebuggingPort() int {
|
||||
func (b *Browser) Close() error {
|
||||
var err error
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
if runtime.GOOS != goosWindows {
|
||||
err = b.cmd.Process.Signal(os.Interrupt)
|
||||
} else {
|
||||
err = b.cmd.Process.Kill()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = b.cmd.Process.Wait()
|
||||
|
||||
if err != nil {
|
||||
|
@ -2,9 +2,10 @@ package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Flags map[string]interface{}
|
||||
@ -61,8 +62,6 @@ func (flags Flags) Has(arg string) bool {
|
||||
}
|
||||
|
||||
func (flags Flags) List() []string {
|
||||
var list []string
|
||||
|
||||
orderedFlags := make([]string, 0, 10)
|
||||
|
||||
for arg := range flags {
|
||||
@ -71,23 +70,25 @@ func (flags Flags) List() []string {
|
||||
|
||||
sort.Strings(orderedFlags)
|
||||
|
||||
for _, arg := range orderedFlags {
|
||||
list := make([]string, len(orderedFlags))
|
||||
|
||||
for i, arg := range orderedFlags {
|
||||
val, err := flags.Get(arg)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch val.(type) {
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
arg = fmt.Sprintf("--%s=%d", arg, val.(int))
|
||||
arg = fmt.Sprintf("--%s=%d", arg, v)
|
||||
case string:
|
||||
arg = fmt.Sprintf("--%s=%s", arg, val.(string))
|
||||
arg = fmt.Sprintf("--%s=%s", arg, v)
|
||||
default:
|
||||
arg = fmt.Sprintf("--%s", arg)
|
||||
}
|
||||
|
||||
list = append(list, arg)
|
||||
list[i] = arg
|
||||
}
|
||||
|
||||
return list
|
||||
|
@ -1,29 +1,28 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func resolveExecutablePath() string {
|
||||
var res string
|
||||
func resolveExecutablePath() (path string) {
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
case goosDarwin:
|
||||
for _, c := range []string{
|
||||
"/Applications/Google Chrome Canary.app",
|
||||
"/Applications/Google Chrome.app",
|
||||
} {
|
||||
// MacOS apps are actually folders
|
||||
if info, err := os.Stat(c); err == nil && info.IsDir() {
|
||||
res = fmt.Sprintf("open %s -n", c)
|
||||
info, err := os.Stat(c)
|
||||
if err == nil && info.IsDir() {
|
||||
path = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case "linux":
|
||||
case goosLinux:
|
||||
for _, c := range []string{
|
||||
"headless_shell",
|
||||
"chromium",
|
||||
@ -31,13 +30,13 @@ func resolveExecutablePath() string {
|
||||
"google-chrome-unstable",
|
||||
"google-chrome-stable"} {
|
||||
if _, err := exec.LookPath(c); err == nil {
|
||||
res = c
|
||||
path = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case "windows":
|
||||
case goosWindows:
|
||||
}
|
||||
|
||||
return res
|
||||
return
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func Launch(setters ...Option) (*Browser, error) {
|
||||
@ -40,7 +41,7 @@ func Launch(setters ...Option) (*Browser, error) {
|
||||
flags.SetN("mute-audio")
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if runtime.GOOS == goosWindows {
|
||||
flags.SetN("disable-gpu")
|
||||
}
|
||||
|
||||
@ -78,7 +79,10 @@ func Launch(setters ...Option) (*Browser, error) {
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(chromeExecutable, flags.List()...)
|
||||
execArgs := []string{chromeExecutable, "--args"}
|
||||
execArgs = append(execArgs, flags.List()...)
|
||||
|
||||
cmd := exec.Command("open", execArgs...)
|
||||
cmd.Dir = workDir
|
||||
|
||||
err = cmd.Start()
|
||||
|
@ -18,6 +18,12 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
goosWindows = "windows"
|
||||
goosLinux = "linux"
|
||||
goosDarwin = "darwin"
|
||||
)
|
||||
|
||||
func WithoutDefaultArgs() Option {
|
||||
return func(opts *Options) {
|
||||
opts.ignoreDefaultArgs = true
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func ExecFile(pathToFile string, opts Options) {
|
||||
@ -38,9 +37,10 @@ func Exec(query string, opts Options) {
|
||||
|
||||
l := NewLogger()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := opts.WithContext(context.Background())
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGHUP)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
@ -59,12 +59,9 @@ func Exec(query string, opts Options) {
|
||||
|
||||
out, err := prog.Run(
|
||||
ctx,
|
||||
runtime.WithBrowser(opts.Cdp),
|
||||
runtime.WithLog(l),
|
||||
runtime.WithLogLevel(logging.DebugLevel),
|
||||
runtime.WithParams(opts.Params),
|
||||
runtime.WithProxy(opts.Proxy),
|
||||
runtime.WithUserAgent(opts.UserAgent),
|
||||
)
|
||||
|
||||
if opts.ShowTime {
|
||||
|
@ -1,9 +1,50 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/drivers"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/cdp"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/http"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Cdp string
|
||||
Params map[string]interface{}
|
||||
Proxy string
|
||||
UserAgent string
|
||||
ShowTime bool
|
||||
Cdp string
|
||||
Params map[string]interface{}
|
||||
Proxy string
|
||||
UserAgent string
|
||||
ShowTime bool
|
||||
KeepCookies bool
|
||||
}
|
||||
|
||||
func (opts Options) WithContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
httpDriver := http.NewDriver(
|
||||
http.WithProxy(opts.Proxy),
|
||||
http.WithUserAgent(opts.UserAgent),
|
||||
)
|
||||
|
||||
ctx = drivers.WithContext(
|
||||
ctx,
|
||||
httpDriver,
|
||||
drivers.AsDefault(),
|
||||
)
|
||||
|
||||
cdpOpts := []cdp.Option{
|
||||
cdp.WithAddress(opts.Cdp),
|
||||
cdp.WithProxy(opts.Proxy),
|
||||
cdp.WithUserAgent(opts.UserAgent),
|
||||
}
|
||||
|
||||
if opts.KeepCookies {
|
||||
cdpOpts = append(cdpOpts, cdp.WithKeepCookies())
|
||||
}
|
||||
|
||||
cdpDriver := cdp.NewDriver(cdpOpts...)
|
||||
|
||||
ctx = drivers.WithContext(
|
||||
ctx,
|
||||
cdpDriver,
|
||||
)
|
||||
|
||||
return context.WithCancel(ctx)
|
||||
}
|
||||
|
35
cli/repl.go
35
cli/repl.go
@ -3,14 +3,16 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/parser/fql"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/compiler"
|
||||
"github.com/MontFerret/ferret/pkg/runtime"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/logging"
|
||||
"github.com/chzyer/readline"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func Repl(version string, opts Options) {
|
||||
@ -23,6 +25,11 @@ func Repl(version string, opts Options) {
|
||||
Prompt: "> ",
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "exit",
|
||||
AutoComplete: NewAutoCompleter(
|
||||
append(
|
||||
fqlLiterals(),
|
||||
ferret.RegisteredFunctions()...,
|
||||
)),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -42,9 +49,10 @@ func Repl(version string, opts Options) {
|
||||
|
||||
l := NewLogger()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := opts.WithContext(context.Background())
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGHUP)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
exit := func() {
|
||||
cancel()
|
||||
@ -67,7 +75,7 @@ func Repl(version string, opts Options) {
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if len(line) == 0 {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -110,12 +118,9 @@ func Repl(version string, opts Options) {
|
||||
|
||||
out, err := program.Run(
|
||||
ctx,
|
||||
runtime.WithBrowser(opts.Cdp),
|
||||
runtime.WithLog(l),
|
||||
runtime.WithLogLevel(logging.DebugLevel),
|
||||
runtime.WithParams(opts.Params),
|
||||
runtime.WithProxy(opts.Proxy),
|
||||
runtime.WithUserAgent(opts.UserAgent),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@ -132,3 +137,13 @@ func Repl(version string, opts Options) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fqlLiterals() (literals []string) {
|
||||
lns := fql.NewFqlLexer(nil).LiteralNames
|
||||
|
||||
for _, ln := range lns {
|
||||
literals = append(literals, strings.Trim(ln, "'"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
76
e2e/main.go
76
e2e/main.go
@ -8,7 +8,9 @@ import (
|
||||
"github.com/MontFerret/ferret/e2e/server"
|
||||
"github.com/rs/zerolog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -24,17 +26,17 @@ var (
|
||||
"root directory with test pages",
|
||||
)
|
||||
|
||||
port = flag.Uint64(
|
||||
"port",
|
||||
8080,
|
||||
"server port",
|
||||
)
|
||||
|
||||
cdp = flag.String(
|
||||
"cdp",
|
||||
"http://0.0.0.0:9222",
|
||||
"address of remote Chrome instance",
|
||||
)
|
||||
|
||||
filter = flag.String(
|
||||
"filter",
|
||||
"",
|
||||
"regexp expression to filter out tests",
|
||||
)
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -42,17 +44,40 @@ func main() {
|
||||
|
||||
logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
s := server.New(server.Settings{
|
||||
Port: *port,
|
||||
Dir: *pagesDir,
|
||||
staticPort := uint64(8080)
|
||||
static := server.New(server.Settings{
|
||||
Port: staticPort,
|
||||
Dir: filepath.Join(*pagesDir, "static"),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
dynamicPort := uint64(8081)
|
||||
dynamic := server.New(server.Settings{
|
||||
Port: dynamicPort,
|
||||
Dir: filepath.Join(*pagesDir, "dynamic"),
|
||||
})
|
||||
|
||||
var filterR *regexp.Regexp
|
||||
|
||||
if *filter != "" {
|
||||
r, err := regexp.Compile(*filter)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filterR = r
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := s.Start(); err != nil {
|
||||
logger.Info().Timestamp().Msg("shutting down the server")
|
||||
if err := static.Start(); err != nil {
|
||||
logger.Info().Timestamp().Msg("shutting down the static pages server")
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := dynamic.Start(); err != nil {
|
||||
logger.Info().Timestamp().Msg("shutting down the dynamic pages server")
|
||||
}
|
||||
}()
|
||||
|
||||
@ -71,18 +96,29 @@ func main() {
|
||||
}
|
||||
|
||||
r := runner.New(logger, runner.Settings{
|
||||
ServerAddress: fmt.Sprintf("http://0.0.0.0:%d", *port),
|
||||
CDPAddress: *cdp,
|
||||
Dir: *testsDir,
|
||||
StaticServerAddress: fmt.Sprintf("http://0.0.0.0:%d", staticPort),
|
||||
DynamicServerAddress: fmt.Sprintf("http://0.0.0.0:%d", dynamicPort),
|
||||
CDPAddress: *cdp,
|
||||
Dir: *testsDir,
|
||||
Filter: filterR,
|
||||
})
|
||||
|
||||
err := r.Run()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
if err := s.Stop(ctx); err != nil {
|
||||
logger.Fatal().Timestamp().Err(err).Msg("failed to stop server")
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
<-c
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
err := r.Run(ctx)
|
||||
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
@ -1,565 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0049) -->
|
||||
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="Components and options for laying out your Bootstrap project, including wrapping containers, a powerful grid system, a flexible media object, and responsive utility classes.">
|
||||
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
|
||||
<meta name="generator" content="Jekyll v3.8.3">
|
||||
|
||||
<title>Overview · Bootstrap</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
|
||||
<style class="anchorjs"></style><link href="./assets/bootstrap.min.css" rel="stylesheet" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
|
||||
|
||||
<!-- Documentation extras -->
|
||||
|
||||
<link href="./assets/docsearch.min.css" rel="stylesheet">
|
||||
|
||||
<link href="./assets/docs.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="apple-touch-icon" href="http://getbootstrap.com/docs/4.1/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="icon" href="http://getbootstrap.com/docs/4.1/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
|
||||
<link rel="icon" href="http://getbootstrap.com/docs/4.1/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
|
||||
<link rel="manifest" href="http://getbootstrap.com/docs/4.1/assets/img/favicons/manifest.json">
|
||||
<link rel="mask-icon" href="http://getbootstrap.com/docs/4.1/assets/img/favicons/safari-pinned-tab.svg" color="#563d7c">
|
||||
<link rel="icon" href="http://getbootstrap.com/favicon.ico">
|
||||
<meta name="msapplication-config" content="/docs/4.1/assets/img/favicons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#563d7c">
|
||||
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:site" content="@getbootstrap">
|
||||
<meta name="twitter:creator" content="@getbootstrap">
|
||||
<meta name="twitter:title" content="Overview">
|
||||
<meta name="twitter:description" content="Components and options for laying out your Bootstrap project, including wrapping containers, a powerful grid system, a flexible media object, and responsive utility classes.">
|
||||
<meta name="twitter:image" content="https://getbootstrap.com/docs/4.1/assets/brand/bootstrap-social-logo.png">
|
||||
|
||||
<!-- Facebook -->
|
||||
<meta property="og:url" content="https://getbootstrap.com/docs/4.1/layout/overview/">
|
||||
<meta property="og:title" content="Overview">
|
||||
<meta property="og:description" content="Components and options for laying out your Bootstrap project, including wrapping containers, a powerful grid system, a flexible media object, and responsive utility classes.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="http://getbootstrap.com/docs/4.1/assets/brand/bootstrap-social.png">
|
||||
<meta property="og:image:secure_url" content="https://getbootstrap.com/docs/4.1/assets/brand/bootstrap-social.png">
|
||||
<meta property="og:image:type" content="image/png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
|
||||
|
||||
<script>
|
||||
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
|
||||
ga('create', 'UA-146052-10', 'getbootstrap.com');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
<script async="" src="./assets/analytics.js"></script>
|
||||
|
||||
<script id="_carbonads_projs" type="text/javascript" src="./assets/CKYIKKJL.json"></script></head>
|
||||
<body>
|
||||
<a id="skippy" class="sr-only sr-only-focusable" href="#content">
|
||||
<div class="container">
|
||||
<span class="skiplink-text">Skip to main content</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bd-navbar">
|
||||
<a class="navbar-brand mr-0 mr-md-2" href="http://getbootstrap.com/" aria-label="Bootstrap"><svg class="d-block" width="36" height="36" viewBox="0 0 612 612" xmlns="http://www.w3.org/2000/svg" focusable="false"><title>Bootstrap</title><path fill="currentColor" d="M510 8a94.3 94.3 0 0 1 94 94v408a94.3 94.3 0 0 1-94 94H102a94.3 94.3 0 0 1-94-94V102a94.3 94.3 0 0 1 94-94h408m0-8H102C45.9 0 0 45.9 0 102v408c0 56.1 45.9 102 102 102h408c56.1 0 102-45.9 102-102V102C612 45.9 566.1 0 510 0z"></path><path fill="currentColor" d="M196.77 471.5V154.43h124.15c54.27 0 91 31.64 91 79.1 0 33-24.17 63.72-54.71 69.21v1.76c43.07 5.49 70.75 35.82 70.75 78 0 55.81-40 89-107.45 89zm39.55-180.4h63.28c46.8 0 72.29-18.68 72.29-53 0-31.42-21.53-48.78-60-48.78h-75.57zm78.22 145.46c47.68 0 72.73-19.34 72.73-56s-25.93-55.37-76.46-55.37h-74.49v111.4z"></path></svg>
|
||||
</a>
|
||||
|
||||
<div class="navbar-nav-scroll">
|
||||
<ul class="navbar-nav bd-navbar-nav flex-row">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link " href="http://getbootstrap.com/" onclick="ga('send', 'event', 'Navbar', 'Community links', 'Bootstrap');">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="http://getbootstrap.com/docs/4.1/getting-started/introduction/" onclick="ga('send', 'event', 'Navbar', 'Community links', 'Docs');">Documentation</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link " href="http://getbootstrap.com/docs/4.1/examples/" onclick="ga('send', 'event', 'Navbar', 'Community links', 'Examples');">Examples</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://themes.getbootstrap.com/" onclick="ga('send', 'event', 'Navbar', 'Community links', 'Themes');" target="_blank" rel="noopener">Themes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://expo.getbootstrap.com/" onclick="ga('send', 'event', 'Navbar', 'Community links', 'Expo');" target="_blank" rel="noopener">Expo</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://blog.getbootstrap.com/" onclick="ga('send', 'event', 'Navbar', 'Community links', 'Blog');" target="_blank" rel="noopener">Blog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-item nav-link dropdown-toggle mr-md-2" href="#" id="bd-versions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
v4.1
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bd-versions">
|
||||
<a class="dropdown-item active" href="http://getbootstrap.com/docs/4.1/">Latest (4.1.x)</a>
|
||||
<a class="dropdown-item" href="https://getbootstrap.com/docs/4.0/">v4.0.0</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="https://v4-alpha.getbootstrap.com/">v4 Alpha 6</a>
|
||||
<a class="dropdown-item" href="https://getbootstrap.com/docs/3.3/">v3.3.7</a>
|
||||
<a class="dropdown-item" href="https://getbootstrap.com/2.3.2/">v2.3.2</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link p-2" href="https://github.com/twbs/bootstrap" target="_blank" rel="noopener" aria-label="GitHub"><svg class="navbar-nav-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false"><title>GitHub</title><path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="currentColor" fill-rule="evenodd"></path></svg>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link p-2" href="https://twitter.com/getbootstrap" target="_blank" rel="noopener" aria-label="Twitter"><svg class="navbar-nav-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 416.32" focusable="false"><title>Twitter</title><path d="M160.83 416.32c193.2 0 298.92-160.22 298.92-298.92 0-4.51 0-9-.2-13.52A214 214 0 0 0 512 49.38a212.93 212.93 0 0 1-60.44 16.6 105.7 105.7 0 0 0 46.3-58.19 209 209 0 0 1-66.79 25.37 105.09 105.09 0 0 0-181.73 71.91 116.12 116.12 0 0 0 2.66 24c-87.28-4.3-164.73-46.3-216.56-109.82A105.48 105.48 0 0 0 68 159.6a106.27 106.27 0 0 1-47.53-13.11v1.43a105.28 105.28 0 0 0 84.21 103.06 105.67 105.67 0 0 1-47.33 1.84 105.06 105.06 0 0 0 98.14 72.94A210.72 210.72 0 0 1 25 370.84a202.17 202.17 0 0 1-25-1.43 298.85 298.85 0 0 0 160.83 46.92" fill="currentColor"></path></svg>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link p-2" href="https://bootstrap-slack.herokuapp.com/" target="_blank" rel="noopener" aria-label="Slack"><svg class="navbar-nav-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" focusable="false"><title>Slack</title><path fill="currentColor" d="M210.787 234.832l68.31-22.883 22.1 65.977-68.309 22.882z"></path><path d="M490.54 185.6C437.7 9.59 361.6-31.34 185.6 21.46S-31.3 150.4 21.46 326.4 150.4 543.3 326.4 490.54 543.34 361.6 490.54 185.6zM401.7 299.8l-33.15 11.05 11.46 34.38c4.5 13.92-2.87 29.06-16.78 33.56-2.87.82-6.14 1.64-9 1.23a27.32 27.32 0 0 1-24.56-18l-11.46-34.38-68.36 22.92 11.46 34.38c4.5 13.92-2.87 29.06-16.78 33.56-2.87.82-6.14 1.64-9 1.23a27.32 27.32 0 0 1-24.56-18l-11.46-34.43-33.15 11.05c-2.87.82-6.14 1.64-9 1.23a27.32 27.32 0 0 1-24.56-18c-4.5-13.92 2.87-29.06 16.78-33.56l33.12-11.03-22.1-65.9-33.15 11.05c-2.87.82-6.14 1.64-9 1.23a27.32 27.32 0 0 1-24.56-18c-4.48-13.93 2.89-29.07 16.81-33.58l33.15-11.05-11.46-34.38c-4.5-13.92 2.87-29.06 16.78-33.56s29.06 2.87 33.56 16.78l11.46 34.38 68.36-22.92-11.46-34.38c-4.5-13.92 2.87-29.06 16.78-33.56s29.06 2.87 33.56 16.78l11.47 34.42 33.15-11.05c13.92-4.5 29.06 2.87 33.56 16.78s-2.87 29.06-16.78 33.56L329.7 194.6l22.1 65.9 33.15-11.05c13.92-4.5 29.06 2.87 33.56 16.78s-2.88 29.07-16.81 33.57z" fill="currentColor"></path></svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a class="btn btn-bd-download d-none d-lg-inline-block mb-3 mb-md-0 ml-md-3" href="http://getbootstrap.com/docs/4.1/getting-started/download/">Download</a>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row flex-xl-nowrap">
|
||||
<div class="col-12 col-md-3 col-xl-2 bd-sidebar">
|
||||
<form class="bd-search d-flex align-items-center">
|
||||
<span class="algolia-autocomplete" style="position: relative; display: inline-block; direction: ltr;"><input type="search" class="form-control ds-input" id="search-input" placeholder="Search..." autocomplete="off" data-siteurl="https://getbootstrap.com" data-docs-version="4.1" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-owns="algolia-autocomplete-listbox-0" dir="auto" style="position: relative; vertical-align: top;"><pre aria-hidden="true" style="position: absolute; visibility: hidden; white-space: pre; font-family: -apple-system, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400; word-spacing: 0px; letter-spacing: normal; text-indent: 0px; text-rendering: auto; text-transform: none;"></pre><span class="ds-dropdown-menu" role="listbox" id="algolia-autocomplete-listbox-0" style="position: absolute; top: 100%; z-index: 100; display: none; left: 0px; right: auto;"><div class="ds-dataset-1"></div></span></span>
|
||||
<button class="btn btn-link bd-search-docs-toggle d-md-none p-0 ml-3" type="button" data-toggle="collapse" data-target="#bd-docs-nav" aria-controls="bd-docs-nav" aria-expanded="false" aria-label="Toggle docs navigation"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="30" height="30" focusable="false"><title>Menu</title><path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-miterlimit="10" d="M4 7h22M4 15h22M4 23h22"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<nav class="collapse bd-links" id="bd-docs-nav"><div class="bd-toc-item">
|
||||
<a class="bd-toc-link" href="http://getbootstrap.com/docs/4.1/getting-started/introduction/">
|
||||
Getting started
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/introduction/">
|
||||
Introduction
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/download/">
|
||||
Download
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/contents/">
|
||||
Contents
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/browsers-devices/">
|
||||
Browsers & devices
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/javascript/">
|
||||
JavaScript
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/theming/">
|
||||
Theming
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/build-tools/">
|
||||
Build tools
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/webpack/">
|
||||
Webpack
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/getting-started/accessibility/">
|
||||
Accessibility
|
||||
</a></li></ul>
|
||||
</div><div class="bd-toc-item active">
|
||||
<a class="bd-toc-link" href="">
|
||||
Layout
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"><li class="active bd-sidenav-active">
|
||||
<a href="overview.html">
|
||||
Overview
|
||||
</a></li><li>
|
||||
<a href="grid.html">
|
||||
Grid
|
||||
</a></li><li>
|
||||
<a href="media.html">
|
||||
Media object
|
||||
</a></li><li>
|
||||
<a href="utilities.html">
|
||||
Utilities for layout
|
||||
</a></li></ul>
|
||||
</div><div class="bd-toc-item">
|
||||
<a class="bd-toc-link" href="http://getbootstrap.com/docs/4.1/content/reboot/">
|
||||
Content
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/content/reboot/">
|
||||
Reboot
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/content/typography/">
|
||||
Typography
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/content/code/">
|
||||
Code
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/content/images/">
|
||||
Images
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/content/tables/">
|
||||
Tables
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/content/figures/">
|
||||
Figures
|
||||
</a></li></ul>
|
||||
</div><div class="bd-toc-item">
|
||||
<a class="bd-toc-link" href="http://getbootstrap.com/docs/4.1/components/alerts/">
|
||||
Components
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/alerts/">
|
||||
Alerts
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/badge/">
|
||||
Badge
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/breadcrumb/">
|
||||
Breadcrumb
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/buttons/">
|
||||
Buttons
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/button-group/">
|
||||
Button group
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/card/">
|
||||
Card
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/carousel/">
|
||||
Carousel
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/collapse/">
|
||||
Collapse
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/dropdowns/">
|
||||
Dropdowns
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/forms/">
|
||||
Forms
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/input-group/">
|
||||
Input group
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/jumbotron/">
|
||||
Jumbotron
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/list-group/">
|
||||
List group
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/modal/">
|
||||
Modal
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/navs/">
|
||||
Navs
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/navbar/">
|
||||
Navbar
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/pagination/">
|
||||
Pagination
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/popovers/">
|
||||
Popovers
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/progress/">
|
||||
Progress
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/scrollspy/">
|
||||
Scrollspy
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/components/tooltips/">
|
||||
Tooltips
|
||||
</a></li></ul>
|
||||
</div><div class="bd-toc-item">
|
||||
<a class="bd-toc-link" href="http://getbootstrap.com/docs/4.1/utilities/borders/">
|
||||
Utilities
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/borders/">
|
||||
Borders
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/clearfix/">
|
||||
Clearfix
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/close-icon/">
|
||||
Close icon
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/colors/">
|
||||
Colors
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/display/">
|
||||
Display
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/embed/">
|
||||
Embed
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/flex/">
|
||||
Flex
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/float/">
|
||||
Float
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/image-replacement/">
|
||||
Image replacement
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/position/">
|
||||
Position
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/screenreaders/">
|
||||
Screenreaders
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/shadows/">
|
||||
Shadows
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/sizing/">
|
||||
Sizing
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/spacing/">
|
||||
Spacing
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/text/">
|
||||
Text
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/vertical-align/">
|
||||
Vertical align
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/utilities/visibility/">
|
||||
Visibility
|
||||
</a></li></ul>
|
||||
</div><div class="bd-toc-item">
|
||||
<a class="bd-toc-link" href="http://getbootstrap.com/docs/4.1/extend/approach/">
|
||||
Extend
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/extend/approach/">
|
||||
Approach
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/extend/icons/">
|
||||
Icons
|
||||
</a></li></ul>
|
||||
</div><div class="bd-toc-item">
|
||||
<a class="bd-toc-link" href="http://getbootstrap.com/docs/4.1/migration/">
|
||||
Migration
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"></ul>
|
||||
</div><div class="bd-toc-item">
|
||||
<a class="bd-toc-link" href="http://getbootstrap.com/docs/4.1/about/overview/">
|
||||
About
|
||||
</a>
|
||||
|
||||
<ul class="nav bd-sidenav"><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/about/overview/">
|
||||
Overview
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/about/brand/">
|
||||
Brand
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/about/license/">
|
||||
License
|
||||
</a></li><li>
|
||||
<a href="http://getbootstrap.com/docs/4.1/about/translations/">
|
||||
Translations
|
||||
</a></li></ul>
|
||||
</div></nav>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-none d-xl-block col-xl-2 bd-toc">
|
||||
<ul class="section-nav">
|
||||
<li class="toc-entry toc-h2"><a href="#containers">Containers</a></li>
|
||||
<li class="toc-entry toc-h2"><a href="#responsive-breakpoints">Responsive breakpoints</a></li>
|
||||
<li class="toc-entry toc-h2"><a href="#z-index">Z-index</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<main class="col-12 col-md-9 col-xl-8 py-md-3 pl-md-5 bd-content" role="main">
|
||||
<h1 class="bd-title" id="content">Overview</h1>
|
||||
<p class="bd-lead">Components and options for laying out your Bootstrap project, including wrapping containers, a powerful grid system, a flexible media object, and responsive utility classes.</p>
|
||||
<script async="" src="./assets/carbon.js" id="_carbonads_js"></script><div id="carbonads"><span><span class="carbon-wrap"><a href="http://srv.carbonads.net/ads/click/x/GTND42QIF6BI5KQNCV74YKQMCKAIV5QECABDVZ3JCW7ILK7YCASI453KC6BIC5QMCEYDTK3EHJNCLSIZ?segment=placement:getbootstrapcom;" class="carbon-img" target="_blank" rel="noopener"><img src="./assets/1538328304-Slack-pink_logo.png" alt="" border="0" height="100" width="130" style="max-width: 130px;"></a><a href="http://srv.carbonads.net/ads/click/x/GTND42QIF6BI5KQNCV74YKQMCKAIV5QECABDVZ3JCW7ILK7YCASI453KC6BIC5QMCEYDTK3EHJNCLSIZ?segment=placement:getbootstrapcom;" class="carbon-text" target="_blank" rel="noopener">It's teamwork, but simpler, more pleasant and more productive.</a></span><a href="http://carbonads.net/?utm_source=getbootstrapcom&utm_medium=ad_via_link&utm_campaign=in_unit&utm_term=carbon" class="carbon-poweredby" target="_blank" rel="noopener">ads via Carbon</a><img src="./assets/B21259774.226039675" border="0" height="1" width="1" style="display: none;"></span></div>
|
||||
|
||||
<h2 id="containers"><div>Containers<a class="anchorjs-link " href="#containers" aria-label="Anchor" data-anchorjs-icon="#" style="padding-left: 0.375em;"></a></div></h2>
|
||||
|
||||
<p>Containers are the most basic layout element in Bootstrap and are <strong>required when using our default grid system</strong>. Choose from a responsive, fixed-width container (meaning its <code class="highlighter-rouge">max-width</code> changes at each breakpoint) or fluid-width (meaning it’s <code class="highlighter-rouge">100%</code> wide all the time).</p>
|
||||
|
||||
<p>While containers <em>can</em> be nested, most layouts do not require a nested container.</p>
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="bd-example-container">
|
||||
<div class="bd-example-container-header"></div>
|
||||
<div class="bd-example-container-sidebar"></div>
|
||||
<div class="bd-example-container-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt"><div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">></span>
|
||||
<span class="c"><!-- Content here --></span>
|
||||
<span class="nt"></div></span></code></pre></figure>
|
||||
|
||||
<p>Use <code class="highlighter-rouge">.container-fluid</code> for a full width container, spanning the entire width of the viewport.</p>
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="bd-example-container bd-example-container-fluid">
|
||||
<div class="bd-example-container-header"></div>
|
||||
<div class="bd-example-container-sidebar"></div>
|
||||
<div class="bd-example-container-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt"><div</span> <span class="na">class=</span><span class="s">"container-fluid"</span><span class="nt">></span>
|
||||
...
|
||||
<span class="nt"></div></span></code></pre></figure>
|
||||
|
||||
<h2 id="responsive-breakpoints"><div>Responsive breakpoints<a class="anchorjs-link " href="#responsive-breakpoints" aria-label="Anchor" data-anchorjs-icon="#" style="padding-left: 0.375em;"></a></div></h2>
|
||||
|
||||
<p>Since Bootstrap is developed to be mobile first, we use a handful of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries">media queries</a> to create sensible breakpoints for our layouts and interfaces. These breakpoints are mostly based on minimum viewport widths and allow us to scale up elements as the viewport changes.</p>
|
||||
|
||||
<p>Bootstrap primarily uses the following media query ranges—or breakpoints—in our source Sass files for our layout, grid system, and components.</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="c1">// Extra small devices (portrait phones, less than 576px)</span>
|
||||
<span class="c1">// No media query for `xs` since this is the default in Bootstrap</span>
|
||||
|
||||
<span class="c1">// Small devices (landscape phones, 576px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">576px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Medium devices (tablets, 768px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">768px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Large devices (desktops, 992px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">992px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Extra large devices (large desktops, 1200px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">1200px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span></code></pre></figure>
|
||||
|
||||
<p>Since we write our source CSS in Sass, all our media queries are available via Sass mixins:</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="c1">// No media query necessary for xs breakpoint as it's effectively `@media (min-width: 0) { ... }`</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-up</span><span class="p">(</span><span class="n">sm</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-up</span><span class="p">(</span><span class="n">md</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-up</span><span class="p">(</span><span class="n">lg</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-up</span><span class="p">(</span><span class="n">xl</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Example: Hide starting at `min-width: 0`, and then show at the `sm` breakpoint</span>
|
||||
<span class="nc">.custom-class</span> <span class="p">{</span>
|
||||
<span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
|
||||
<span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-up</span><span class="p">(</span><span class="n">sm</span><span class="p">)</span> <span class="p">{</span>
|
||||
<span class="nc">.custom-class</span> <span class="p">{</span>
|
||||
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">}</span></code></pre></figure>
|
||||
|
||||
<p>We occasionally use media queries that go in the other direction (the given screen size <em>or smaller</em>):</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="c1">// Extra small devices (portrait phones, less than 576px)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">575</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Small devices (landscape phones, less than 768px)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">767</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Medium devices (tablets, less than 992px)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">991</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Large devices (desktops, less than 1200px)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">1199</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Extra large devices (large desktops)</span>
|
||||
<span class="c1">// No media query since the extra-large breakpoint has no upper bound on its width</span></code></pre></figure>
|
||||
|
||||
<div class="bd-callout bd-callout-info">
|
||||
<p>Note that since browsers do not currently support <a href="https://www.w3.org/TR/mediaqueries-4/#range-context">range context queries</a>, we work around the limitations of <a href="https://www.w3.org/TR/mediaqueries-4/#mq-min-max"><code class="highlighter-rouge">min-</code> and <code class="highlighter-rouge">max-</code> prefixes</a> and viewports with fractional widths (which can occur under certain conditions on high-dpi devices, for instance) by using values with higher precision for these comparisons.</p>
|
||||
</div>
|
||||
|
||||
<p>Once again, these media queries are also available via Sass mixins:</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="k">@include</span> <span class="nd">media-breakpoint-down</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-down</span><span class="p">(</span><span class="n">sm</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-down</span><span class="p">(</span><span class="n">md</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-down</span><span class="p">(</span><span class="n">lg</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="c1">// No media query necessary for xl breakpoint as it has no upper bound on its width</span>
|
||||
|
||||
<span class="c1">// Example: Style from medium breakpoint and down</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-down</span><span class="p">(</span><span class="n">md</span><span class="p">)</span> <span class="p">{</span>
|
||||
<span class="nc">.custom-class</span> <span class="p">{</span>
|
||||
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">}</span></code></pre></figure>
|
||||
|
||||
<p>There are also media queries and mixins for targeting a single segment of screen sizes using the minimum and maximum breakpoint widths.</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="c1">// Extra small devices (portrait phones, less than 576px)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">575</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Small devices (landscape phones, 576px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">576px</span><span class="p">)</span> <span class="nf">and</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">767</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Medium devices (tablets, 768px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">768px</span><span class="p">)</span> <span class="nf">and</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">991</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Large devices (desktops, 992px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">992px</span><span class="p">)</span> <span class="nf">and</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">1199</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
|
||||
<span class="c1">// Extra large devices (large desktops, 1200px and up)</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">1200px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span></code></pre></figure>
|
||||
|
||||
<p>These media queries are also available via Sass mixins:</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="k">@include</span> <span class="nd">media-breakpoint-only</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-only</span><span class="p">(</span><span class="n">sm</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-only</span><span class="p">(</span><span class="n">md</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-only</span><span class="p">(</span><span class="n">lg</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span>
|
||||
<span class="k">@include</span> <span class="nd">media-breakpoint-only</span><span class="p">(</span><span class="n">xl</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span></code></pre></figure>
|
||||
|
||||
<p>Similarly, media queries may span multiple breakpoint widths:</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="c1">// Example</span>
|
||||
<span class="c1">// Apply styles starting from medium devices and up to extra large devices</span>
|
||||
<span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">768px</span><span class="p">)</span> <span class="nf">and</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">1199</span><span class="mi">.98px</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span></code></pre></figure>
|
||||
|
||||
<p>The Sass mixin for targeting the same screen size range would be:</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="k">@include</span> <span class="nd">media-breakpoint-between</span><span class="p">(</span><span class="n">md</span><span class="o">,</span> <span class="n">xl</span><span class="p">)</span> <span class="p">{</span> <span class="nc">...</span> <span class="p">}</span></code></pre></figure>
|
||||
|
||||
<h2 id="z-index"><div>Z-index<a class="anchorjs-link " href="#z-index" aria-label="Anchor" data-anchorjs-icon="#" style="padding-left: 0.375em;"></a></div></h2>
|
||||
|
||||
<p>Several Bootstrap components utilize <code class="highlighter-rouge">z-index</code>, the CSS property that helps control layout by providing a third axis to arrange content. We utilize a default z-index scale in Bootstrap that’s been designed to properly layer navigation, tooltips and popovers, modals, and more.</p>
|
||||
|
||||
<p>These higher values start at an arbitrary number, high and specific enough to ideally avoid conflicts. We need a standard set of these across our layered components—tooltips, popovers, navbars, dropdowns, modals—so we can be reasonably consistent in the behaviors. There’s no reason we couldn’t have used <code class="highlighter-rouge">100</code>+ or <code class="highlighter-rouge">500</code>+.</p>
|
||||
|
||||
<p>We don’t encourage customization of these individual values; should you change one, you likely need to change them all.</p>
|
||||
|
||||
<div class="bd-clipboard"><button class="btn-clipboard" title="" data-original-title="Copy to clipboard">Copy</button></div><figure class="highlight"><pre><code class="language-scss" data-lang="scss"><span class="nv">$zindex-dropdown</span><span class="p">:</span> <span class="m">1000</span> <span class="o">!</span><span class="nb">default</span><span class="p">;</span>
|
||||
<span class="nv">$zindex-sticky</span><span class="p">:</span> <span class="m">1020</span> <span class="o">!</span><span class="nb">default</span><span class="p">;</span>
|
||||
<span class="nv">$zindex-fixed</span><span class="p">:</span> <span class="m">1030</span> <span class="o">!</span><span class="nb">default</span><span class="p">;</span>
|
||||
<span class="nv">$zindex-modal-backdrop</span><span class="p">:</span> <span class="m">1040</span> <span class="o">!</span><span class="nb">default</span><span class="p">;</span>
|
||||
<span class="nv">$zindex-modal</span><span class="p">:</span> <span class="m">1050</span> <span class="o">!</span><span class="nb">default</span><span class="p">;</span>
|
||||
<span class="nv">$zindex-popover</span><span class="p">:</span> <span class="m">1060</span> <span class="o">!</span><span class="nb">default</span><span class="p">;</span>
|
||||
<span class="nv">$zindex-tooltip</span><span class="p">:</span> <span class="m">1070</span> <span class="o">!</span><span class="nb">default</span><span class="p">;</span></code></pre></figure>
|
||||
|
||||
<p>To handle overlapping borders within components (e.g., buttons and inputs in input groups), we use low single digit <code class="highlighter-rouge">z-index</code> values of <code class="highlighter-rouge">1</code>, <code class="highlighter-rouge">2</code>, and <code class="highlighter-rouge">3</code> for default, hover, and active states. On hover/focus/active, we bring a particular element to the forefront with a higher <code class="highlighter-rouge">z-index</code> value to show their border over the sibling elements.</p>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./assets/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script>window.jQuery || document.write('<script src="/assets/js/vendor/jquery-slim.min.js"><\/script>')</script>
|
||||
|
||||
<script src="./assets/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script><script src="./assets/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script><script src="./assets/docsearch.min.js"></script><script src="./assets/docs.min.js"></script>
|
||||
|
||||
|
||||
</body></html>
|
34
e2e/pages/dynamic/components/app.js
Normal file
34
e2e/pages/dynamic/components/app.js
Normal file
@ -0,0 +1,34 @@
|
||||
import Layout from './layout.js';
|
||||
import IndexPage from './pages/index.js';
|
||||
import FormsPage from './pages/forms/index.js';
|
||||
import EventsPage from './pages/events/index.js';
|
||||
|
||||
const e = React.createElement;
|
||||
const Router = ReactRouter.Router;
|
||||
const Switch = ReactRouter.Switch;
|
||||
const Route = ReactRouter.Route;
|
||||
const Redirect = ReactRouter.Redirect;
|
||||
const createBrowserHistory = History.createBrowserHistory;
|
||||
|
||||
export default function AppComponent({ redirect = null}) {
|
||||
return e(Router, { history: createBrowserHistory() },
|
||||
e(Layout, null, [
|
||||
e(Switch, null, [
|
||||
e(Route, {
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: IndexPage
|
||||
}),
|
||||
e(Route, {
|
||||
path: '/forms',
|
||||
component: FormsPage
|
||||
}),
|
||||
e(Route, {
|
||||
path: '/events',
|
||||
component: EventsPage
|
||||
}),
|
||||
]),
|
||||
redirect ? e(Redirect, { to: redirect }) : null
|
||||
])
|
||||
)
|
||||
}
|
27
e2e/pages/dynamic/components/layout.js
Normal file
27
e2e/pages/dynamic/components/layout.js
Normal file
@ -0,0 +1,27 @@
|
||||
const e = React.createElement;
|
||||
const NavLink = ReactRouterDOM.NavLink;
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return e("div", { id: "layout"}, [
|
||||
e("nav", { className: "navbar navbar-expand-md navbar-dark bg-dark mb-4", id: "navbar" }, [
|
||||
e(NavLink, { className: "navbar-brand", to: "/"}, "Ferret"),
|
||||
e("button", { className: "navbar-toggler", type: "button"}, [
|
||||
e("span", { className: "navbar-toggler-icon" })
|
||||
]),
|
||||
e("div", { className: "collapse navbar-collapse" }, [
|
||||
e("ul", { className: "navbar-nav mr-auto" }, [
|
||||
e("li", { className: "nav-item"}, [
|
||||
e(NavLink, { className: "nav-link", to: "/forms" }, "Forms")
|
||||
]),
|
||||
e("li", { className: "nav-item"}, [
|
||||
e(NavLink, { className: "nav-link", to: "/navigation" }, "Navigation")
|
||||
]),
|
||||
e("li", { className: "nav-item"}, [
|
||||
e(NavLink, { className: "nav-link", to: "/events" }, "Events")
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
e("main", { className: "container"}, children)
|
||||
])
|
||||
}
|
71
e2e/pages/dynamic/components/pages/events/appearable.js
Normal file
71
e2e/pages/dynamic/components/pages/events/appearable.js
Normal file
@ -0,0 +1,71 @@
|
||||
import random from "../../../utils/random.js";
|
||||
|
||||
const e = React.createElement;
|
||||
|
||||
function render(id, props = {}) {
|
||||
return e("span", { id: `${id}-content`, ...props }, ["Hello world"]);
|
||||
}
|
||||
|
||||
export default class AppearableComponent extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let element = null;
|
||||
|
||||
if (props.appear) {
|
||||
if (props.useStyle) {
|
||||
element = render(props.id, { style: {display: "none"}})
|
||||
}
|
||||
} else {
|
||||
if (props.useStyle) {
|
||||
element = render(props.id, { style: {display: "block" }})
|
||||
} else {
|
||||
element = render(props.id)
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
element
|
||||
};
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
setTimeout(() => {
|
||||
const props = this.props;
|
||||
let element = null;
|
||||
|
||||
if (props.appear) {
|
||||
if (props.useStyle) {
|
||||
element = render(props.id, { style: {display: "block" }})
|
||||
} else {
|
||||
element = render(props.id)
|
||||
}
|
||||
} else {
|
||||
if (props.useStyle) {
|
||||
element = render(props.id, { style: {display: "none"}})
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
element,
|
||||
})
|
||||
}, random())
|
||||
}
|
||||
|
||||
render() {
|
||||
const btnId = `${this.props.id}-btn`;
|
||||
|
||||
return e("div", {className: "card"}, [
|
||||
e("div", { className: "card-header"}, [
|
||||
e("button", {
|
||||
id: btnId,
|
||||
className: "btn btn-primary",
|
||||
onClick: this.handleClick.bind(this)
|
||||
}, [
|
||||
this.props.title || "Toggle class"
|
||||
])
|
||||
]),
|
||||
e("div", { className: "card-body"}, this.state.element)
|
||||
]);
|
||||
}
|
||||
}
|
56
e2e/pages/dynamic/components/pages/events/clickable.js
Normal file
56
e2e/pages/dynamic/components/pages/events/clickable.js
Normal file
@ -0,0 +1,56 @@
|
||||
import random from "../../../utils/random.js";
|
||||
|
||||
const e = React.createElement;
|
||||
|
||||
export default class ClickableComponent extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
show: props.show === true
|
||||
};
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
let timeout = 500;
|
||||
|
||||
if (this.props.randomTimeout) {
|
||||
timeout = random();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
show: !this.state.show
|
||||
})
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
render() {
|
||||
const btnId = `${this.props.id}-btn`;
|
||||
const contentId = `${this.props.id}-content`;
|
||||
const classNames = ["alert"];
|
||||
|
||||
if (this.state.show === true) {
|
||||
classNames.push("alert-success");
|
||||
}
|
||||
|
||||
return e("div", {className: "card clickable"}, [
|
||||
e("div", { className: "card-header"}, [
|
||||
e("button", {
|
||||
id: btnId,
|
||||
className: "btn btn-primary",
|
||||
onClick: this.handleClick.bind(this)
|
||||
}, [
|
||||
this.props.title || "Toggle class"
|
||||
])
|
||||
]),
|
||||
e("div", { className: "card-body"}, [
|
||||
e("div", { id: contentId, className: classNames.join(" ")}, [
|
||||
e("p", null, [
|
||||
"Lorem ipsum dolor sit amet."
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
47
e2e/pages/dynamic/components/pages/events/hoverable.js
Normal file
47
e2e/pages/dynamic/components/pages/events/hoverable.js
Normal file
@ -0,0 +1,47 @@
|
||||
const e = React.createElement;
|
||||
|
||||
export default class HoverableComponent extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hovered: false
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseEnter() {
|
||||
this.setState({
|
||||
hovered: true
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseLeave() {
|
||||
this.setState({
|
||||
hovered: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
|
||||
if (this.state.hovered) {
|
||||
content = e("p", { id: "hoverable-content"}, [
|
||||
"Lorem ipsum dolor sit amet."
|
||||
]);
|
||||
}
|
||||
|
||||
return e("div", { className: "card"}, [
|
||||
e("div", {className: "card-header"}, [
|
||||
e("button", {
|
||||
id: "hoverable-btn",
|
||||
className: "btn btn-primary",
|
||||
onMouseEnter: this.handleMouseEnter.bind(this),
|
||||
onMouseLeave: this.handleMouseLeave.bind(this)
|
||||
}, [
|
||||
"Show content"
|
||||
])
|
||||
]),
|
||||
e("div", {className: "card-body"}, content)
|
||||
]);
|
||||
}
|
||||
}
|
79
e2e/pages/dynamic/components/pages/events/index.js
Normal file
79
e2e/pages/dynamic/components/pages/events/index.js
Normal file
@ -0,0 +1,79 @@
|
||||
import Hoverable from "./hoverable.js";
|
||||
import Clickable from "./clickable.js";
|
||||
import Appearable from "./appearable.js";
|
||||
|
||||
const e = React.createElement;
|
||||
|
||||
export default class EventsPage extends React.Component {
|
||||
render() {
|
||||
return e("div", { id: "page-events" }, [
|
||||
e("div", { className: "row" }, [
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Hoverable),
|
||||
]),
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Clickable, {
|
||||
id: "wait-class",
|
||||
title: "Add class"
|
||||
})
|
||||
]),
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Clickable, {
|
||||
id: "wait-class-random",
|
||||
title: "Add class 2",
|
||||
randomTimeout: true
|
||||
})
|
||||
])
|
||||
]),
|
||||
e("div", { className: "row" }, [
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Clickable, {
|
||||
id: "wait-no-class",
|
||||
title: "Remove class",
|
||||
show: true
|
||||
})
|
||||
]),
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Clickable, {
|
||||
id: "wait-no-class-random",
|
||||
title: "Remove class 2",
|
||||
show: true,
|
||||
randomTimeout: true
|
||||
})
|
||||
]),
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Appearable, {
|
||||
id: "wait-element",
|
||||
appear: true,
|
||||
title: "Appearable"
|
||||
})
|
||||
]),
|
||||
]),
|
||||
e("div", { className: "row" }, [
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Appearable, {
|
||||
id: "wait-no-element",
|
||||
appear: false,
|
||||
title: "Disappearable"
|
||||
})
|
||||
]),
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Appearable, {
|
||||
id: "wait-style",
|
||||
appear: true,
|
||||
title: "Appearable with style",
|
||||
useStyle: true,
|
||||
})
|
||||
]),
|
||||
e("div", { className: "col-lg-4"}, [
|
||||
e(Appearable, {
|
||||
id: "wait-no-style",
|
||||
appear: false,
|
||||
title: "Disappearable",
|
||||
useStyle: true,
|
||||
})
|
||||
]),
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
124
e2e/pages/dynamic/components/pages/forms/index.js
Normal file
124
e2e/pages/dynamic/components/pages/forms/index.js
Normal file
@ -0,0 +1,124 @@
|
||||
const e = React.createElement;
|
||||
|
||||
export default class FormsPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
textInput: "",
|
||||
select: "",
|
||||
multiSelect: "",
|
||||
textarea: ""
|
||||
};
|
||||
|
||||
this.handleTextInput = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
textInput: evt.target.value
|
||||
});
|
||||
};
|
||||
|
||||
this.handleSelect = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
select: evt.target.value
|
||||
});
|
||||
};
|
||||
|
||||
this.handleMultiSelect = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
multiSelect: Array.prototype.map.call(evt.target.selectedOptions, i => i.value).join(", ")
|
||||
});
|
||||
};
|
||||
|
||||
this.handleTtextarea = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
textarea: evt.target.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return e("form", { id: "page-form" }, [
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Text input"),
|
||||
e("input", {
|
||||
id: "text_input",
|
||||
type: "text",
|
||||
className: "form-control",
|
||||
onChange: this.handleTextInput
|
||||
}),
|
||||
e("small", {
|
||||
id: "text_output",
|
||||
className: "form-text text-muted"
|
||||
},
|
||||
this.state.textInput
|
||||
)
|
||||
]),
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Select"),
|
||||
e("select", {
|
||||
id: "select_input",
|
||||
className: "form-control",
|
||||
onChange: this.handleSelect
|
||||
},
|
||||
[
|
||||
e("option", null, 1),
|
||||
e("option", null, 2),
|
||||
e("option", null, 3),
|
||||
e("option", null, 4),
|
||||
e("option", null, 5),
|
||||
]
|
||||
),
|
||||
e("small", {
|
||||
id: "select_output",
|
||||
className: "form-text text-muted"
|
||||
}, this.state.select
|
||||
)
|
||||
]),
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Multi select"),
|
||||
e("select", {
|
||||
id: "multi_select_input",
|
||||
multiple: true,
|
||||
className: "form-control",
|
||||
onChange: this.handleMultiSelect
|
||||
},
|
||||
[
|
||||
e("option", null, 1),
|
||||
e("option", null, 2),
|
||||
e("option", null, 3),
|
||||
e("option", null, 4),
|
||||
e("option", null, 5),
|
||||
]
|
||||
),
|
||||
e("small", {
|
||||
id: "multi_select_output",
|
||||
className: "form-text text-muted"
|
||||
}, this.state.multiSelect
|
||||
)
|
||||
]),
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Textarea"),
|
||||
e("textarea", {
|
||||
id: "textarea_input",
|
||||
rows:"5",
|
||||
className: "form-control",
|
||||
onChange: this.handleTtextarea
|
||||
}
|
||||
),
|
||||
e("small", {
|
||||
id: "textarea_output",
|
||||
className: "form-text text-muted"
|
||||
}, this.state.textarea
|
||||
)
|
||||
]),
|
||||
])
|
||||
}
|
||||
}
|
12
e2e/pages/dynamic/components/pages/index.js
Normal file
12
e2e/pages/dynamic/components/pages/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
const e = React.createElement;
|
||||
|
||||
export default function IndexPage() {
|
||||
return e("div", { className: "jumbotron", "data-type": "page", id: "index" }, [
|
||||
e("div", null,
|
||||
e("h1", null, "Welcome to Ferret E2E test page!")
|
||||
),
|
||||
e("div", null,
|
||||
e("p", { className: "lead" }, "It has several pages for testing different possibilities of the library")
|
||||
)
|
||||
])
|
||||
}
|
4
e2e/pages/dynamic/index.css
Normal file
4
e2e/pages/dynamic/index.css
Normal file
@ -0,0 +1,4 @@
|
||||
/* Show it's not fixed to the top */
|
||||
body {
|
||||
min-height: 75rem;
|
||||
}
|
21
e2e/pages/dynamic/index.html
Normal file
21
e2e/pages/dynamic/index.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Ferret E2E SPA</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div id="root"></div>
|
||||
<script src="https://unpkg.com/react@16.6.1/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@16.6.1/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/history@4.7.2/umd/history.min.js"></script>
|
||||
<script src="https://unpkg.com/react-router@4.3.1/umd/react-router.js"></script>
|
||||
<script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script>
|
||||
<script src="index.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
9
e2e/pages/dynamic/index.js
Normal file
9
e2e/pages/dynamic/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import AppComponent from "./components/app.js";
|
||||
import { parse } from "./utils/qs.js";
|
||||
|
||||
const qs = parse(location.search);
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(AppComponent, qs),
|
||||
document.getElementById("root")
|
||||
);
|
82
e2e/pages/dynamic/utils/qs.js
Normal file
82
e2e/pages/dynamic/utils/qs.js
Normal file
@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
var has = Object.prototype.hasOwnProperty
|
||||
, undef;
|
||||
|
||||
/**
|
||||
* Decode a URI encoded string.
|
||||
*
|
||||
* @param {String} input The URI encoded string.
|
||||
* @returns {String} The decoded string.
|
||||
* @api private
|
||||
*/
|
||||
function decode(input) {
|
||||
return decodeURIComponent(input.replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple query string parser.
|
||||
*
|
||||
* @param {String} query The query string that needs to be parsed.
|
||||
* @returns {Object}
|
||||
* @api public
|
||||
*/
|
||||
export function parse(query) {
|
||||
var parser = /([^=?&]+)=?([^&]*)/g
|
||||
, result = {}
|
||||
, part;
|
||||
|
||||
while (part = parser.exec(query)) {
|
||||
var key = decode(part[1])
|
||||
, value = decode(part[2]);
|
||||
|
||||
//
|
||||
// Prevent overriding of existing properties. This ensures that build-in
|
||||
// methods like `toString` or __proto__ are not overriden by malicious
|
||||
// querystrings.
|
||||
//
|
||||
if (key in result) continue;
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a query string to an object.
|
||||
*
|
||||
* @param {Object} obj Object that should be transformed.
|
||||
* @param {String} prefix Optional prefix.
|
||||
* @returns {String}
|
||||
* @api public
|
||||
*/
|
||||
export function stringify(obj, prefix) {
|
||||
prefix = prefix || '';
|
||||
|
||||
var pairs = []
|
||||
, value
|
||||
, key;
|
||||
|
||||
//
|
||||
// Optionally prefix with a '?' if needed
|
||||
//
|
||||
if ('string' !== typeof prefix) prefix = '?';
|
||||
|
||||
for (key in obj) {
|
||||
if (has.call(obj, key)) {
|
||||
value = obj[key];
|
||||
|
||||
//
|
||||
// Edge cases where we actually want to encode the value to an empty
|
||||
// string instead of the stringified value.
|
||||
//
|
||||
if (!value && (value === null || value === undef || isNaN(value))) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
pairs.push(encodeURIComponent(key) +'='+ encodeURIComponent(value));
|
||||
}
|
||||
}
|
||||
|
||||
return pairs.length ? prefix + pairs.join('&') : '';
|
||||
}
|
13
e2e/pages/dynamic/utils/random.js
Normal file
13
e2e/pages/dynamic/utils/random.js
Normal file
@ -0,0 +1,13 @@
|
||||
export default function random(min = 1000, max = 5000) {
|
||||
const val = Math.random() * 1000 * 10;
|
||||
|
||||
if (val < min) {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (val > max) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
1128
e2e/pages/static/overview.html
Normal file
1128
e2e/pages/static/overview.html
Normal file
File diff suppressed because it is too large
Load Diff
10
e2e/pages/static/simple.html
Normal file
10
e2e/pages/static/simple.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world</h1>
|
||||
</body>
|
||||
</html>
|
47
e2e/pages/static/value.html
Normal file
47
e2e/pages/static/value.html
Normal file
@ -0,0 +1,47 @@
|
||||
<html>
|
||||
<body>
|
||||
<table id="listings_table" align="center" class="tablesorter" style="width: 100%;">
|
||||
<thead><tr><th>some column</th></tr></thead>
|
||||
<tbody><tr id="row_068728"><td>foobar<input type="hidden" class="ListingId" value="068728"></td></tr>
|
||||
<tr id="row_068728" class="odd"><td>foobar<input type="hidden" class="ListingId" value="068728"></td></tr>
|
||||
<tr id="row_816410" class="selectableRow"><td>foobar<input type="hidden" class="ListingId" value="816410"></td></tr>
|
||||
<tr id="row_024413" class="odd"><td>foobar<input type="hidden" class="ListingId" value="52024413"></td></tr>
|
||||
<tr id="row_698690" class=""><td>foobar<input type="hidden" class="ListingId" value="698690"></td></tr>
|
||||
<tr id="row_210583" class="odd"><td>foobar<input type="hidden" class="ListingId" value="210583"></td></tr>
|
||||
<tr id="row_049700" class=""><td>foobar<input type="hidden" class="ListingId" value="049700"></td></tr>
|
||||
<tr id="row_826394" class="odd"><td>foobar<input type="hidden" class="ListingId" value="826394"></td></tr>
|
||||
<tr id="row_354369"><td>foobar<input type="hidden" class="ListingId" value="354369"></td></tr>
|
||||
<tr id="row_135911" class="odd"><td>foobar<input type="hidden" class="ListingId" value="135911"></td></tr>
|
||||
<tr id="row_700285"><td>foobar<input type="hidden" class="ListingId" value="700285"></td></tr>
|
||||
<tr id="row_557242" class="odd"><td>foobar<input type="hidden" class="ListingId" value="557242"></td></tr>
|
||||
<tr id="row_278832"><td>foobar<input type="hidden" class="ListingId" value="278832"></td></tr>
|
||||
<tr id="row_357701" class="odd"><td>foobar<input type="hidden" class="ListingId" value="357701"></td></tr>
|
||||
<tr id="row_313034"><td>foobar<input type="hidden" class="ListingId" value="313034"></td></tr>
|
||||
<tr id="row_959368" class="odd"><td>foobar<input type="hidden" class="ListingId" value="959368"></td></tr>
|
||||
<tr id="row_703500"><td>foobar<input type="hidden" class="ListingId" value="703500"></td></tr>
|
||||
<tr id="row_842750" class="odd"><td>foobar<input type="hidden" class="ListingId" value="842750"></td></tr>
|
||||
<tr id="row_777175"><td>foobar<input type="hidden" class="ListingId" value="777175"></td></tr>
|
||||
<tr id="row_378061" class="odd"><td>foobar<input type="hidden" class="ListingId" value="378061"></td></tr>
|
||||
<tr id="row_072489"><td>foobar<input type="hidden" class="ListingId" value="072489"></td></tr>
|
||||
<tr id="row_383005" class="odd"><td>foobar<input type="hidden" class="ListingId" value="383005"></td></tr>
|
||||
<tr id="row_843393"><td>foobar<input type="hidden" class="ListingId" value="843393"></td></tr>
|
||||
<tr id="row_912263" class="odd"><td>foobar<input type="hidden" class="ListingId" value="59912263"></td></tr>
|
||||
<tr id="row_464535"><td>foobar<input type="hidden" class="ListingId" value="464535"></td></tr>
|
||||
<tr id="row_229710" class="odd"><td>foobar<input type="hidden" class="ListingId" value="229710"></td></tr>
|
||||
<tr id="row_230550"><td>foobar<input type="hidden" class="ListingId" value="230550"></td></tr>
|
||||
<tr id="row_767964" class="odd"><td>foobar<input type="hidden" class="ListingId" value="767964"></td></tr>
|
||||
<tr id="row_758862"><td>foobar<input type="hidden" class="ListingId" value="758862"></td></tr>
|
||||
<tr id="row_944384" class="odd"><td>foobar<input type="hidden" class="ListingId" value="944384"></td></tr>
|
||||
<tr id="row_025449"><td>foobar<input type="hidden" class="ListingId" value="025449"></td></tr>
|
||||
<tr id="row_010245" class="odd"><td>foobar<input type="hidden" class="ListingId" value="010245"></td></tr>
|
||||
<tr id="row_844935"><td>foobar<input type="hidden" class="ListingId" value="844935"></td></tr>
|
||||
<tr id="row_038760" class="odd"><td>foobar<input type="hidden" class="ListingId" value="038760"></td></tr>
|
||||
<tr id="row_013450"><td>foobar<input type="hidden" class="ListingId" value="013450"></td></tr>
|
||||
<tr id="row_124139" class="odd"><td>foobar<input type="hidden" class="ListingId" value="124139"></td></tr>
|
||||
<tr id="row_211145"><td>foobar<input type="hidden" class="ListingId" value="211145"></td></tr>
|
||||
<tr id="row_758761" class="odd"><td>foobar<input type="hidden" class="ListingId" value="758761"></td></tr>
|
||||
<tr id="row_448667"><td>foobar<input type="hidden" class="ListingId" value="448667"></td></tr>
|
||||
<tr id="row_488966" class="odd"><td>foobar<input type="hidden" class="ListingId" value="488966"></td></tr>
|
||||
</tbody></table>
|
||||
</body>
|
||||
</html>
|
@ -3,6 +3,7 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
)
|
||||
@ -24,5 +25,5 @@ func expect(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||
return values.EmptyString, nil
|
||||
}
|
||||
|
||||
return values.NewString(fmt.Sprintf(`expected "%s"", but got "%s"`, args[0], args[1])), nil
|
||||
return values.NewString(fmt.Sprintf(`expected "%s", but got "%s"`, args[0], args[1])), nil
|
||||
}
|
||||
|
@ -3,20 +3,28 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/compiler"
|
||||
"github.com/MontFerret/ferret/pkg/drivers"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/cdp"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/http"
|
||||
"github.com/MontFerret/ferret/pkg/runtime"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Settings struct {
|
||||
ServerAddress string
|
||||
CDPAddress string
|
||||
Dir string
|
||||
StaticServerAddress string
|
||||
DynamicServerAddress string
|
||||
CDPAddress string
|
||||
Dir string
|
||||
Filter *regexp.Regexp
|
||||
}
|
||||
|
||||
Result struct {
|
||||
@ -44,8 +52,19 @@ func New(logger zerolog.Logger, settings Settings) *Runner {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) Run() error {
|
||||
results, err := r.runQueries(r.settings.Dir)
|
||||
func (r *Runner) Run(ctx context.Context) error {
|
||||
ctx = drivers.WithContext(
|
||||
ctx,
|
||||
cdp.NewDriver(cdp.WithAddress(r.settings.CDPAddress)),
|
||||
)
|
||||
|
||||
ctx = drivers.WithContext(
|
||||
ctx,
|
||||
http.NewDriver(),
|
||||
drivers.AsDefault(),
|
||||
)
|
||||
|
||||
results, err := r.runQueries(ctx, r.settings.Dir)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -75,7 +94,7 @@ func (r *Runner) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) runQueries(dir string) ([]Result, error) {
|
||||
func (r *Runner) runQueries(ctx context.Context, dir string) ([]Result, error) {
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
|
||||
if err != nil {
|
||||
@ -91,11 +110,22 @@ func (r *Runner) runQueries(dir string) ([]Result, error) {
|
||||
results := make([]Result, 0, len(files))
|
||||
|
||||
c := compiler.New()
|
||||
c.RegisterFunctions(Assertions())
|
||||
|
||||
if err := c.RegisterFunctions(Assertions()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read scripts
|
||||
for _, f := range files {
|
||||
fName := filepath.Join(dir, f.Name())
|
||||
n := f.Name()
|
||||
|
||||
if r.settings.Filter != nil {
|
||||
if r.settings.Filter.Match([]byte(n)) != true {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fName := filepath.Join(dir, n)
|
||||
b, err := ioutil.ReadFile(fName)
|
||||
|
||||
if err != nil {
|
||||
@ -107,13 +137,30 @@ func (r *Runner) runQueries(dir string) ([]Result, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, r.runQuery(c, fName, string(b)))
|
||||
r.logger.Info().Timestamp().Str("name", fName).Msg("Running test")
|
||||
|
||||
result := r.runQuery(ctx, c, fName, string(b))
|
||||
|
||||
if result.err == nil {
|
||||
r.logger.Info().
|
||||
Timestamp().
|
||||
Str("file", result.name).
|
||||
Msg("Test passed")
|
||||
} else {
|
||||
r.logger.Error().
|
||||
Timestamp().
|
||||
Err(result.err).
|
||||
Str("file", result.name).
|
||||
Msg("Test failed")
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *Runner) runQuery(c *compiler.FqlCompiler, name, script string) Result {
|
||||
func (r *Runner) runQuery(ctx context.Context, c *compiler.FqlCompiler, name, script string) Result {
|
||||
start := time.Now()
|
||||
|
||||
p, err := c.Compile(script)
|
||||
@ -127,9 +174,10 @@ func (r *Runner) runQuery(c *compiler.FqlCompiler, name, script string) Result {
|
||||
}
|
||||
|
||||
out, err := p.Run(
|
||||
context.Background(),
|
||||
runtime.WithBrowser(r.settings.CDPAddress),
|
||||
runtime.WithParam("server", r.settings.ServerAddress),
|
||||
ctx,
|
||||
runtime.WithLog(zerolog.ConsoleWriter{Out: os.Stdout}),
|
||||
runtime.WithParam("static", r.settings.StaticServerAddress),
|
||||
runtime.WithParam("dynamic", r.settings.DynamicServerAddress),
|
||||
)
|
||||
|
||||
duration := time.Now().Sub(start)
|
||||
@ -144,7 +192,13 @@ func (r *Runner) runQuery(c *compiler.FqlCompiler, name, script string) Result {
|
||||
|
||||
var result string
|
||||
|
||||
json.Unmarshal(out, &result)
|
||||
if err := json.Unmarshal(out, &result); err != nil {
|
||||
return Result{
|
||||
name: name,
|
||||
duration: duration,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
return Result{
|
||||
@ -167,20 +221,9 @@ func (r *Runner) report(results []Result) Summary {
|
||||
|
||||
for _, res := range results {
|
||||
if res.err != nil {
|
||||
r.logger.Error().
|
||||
Timestamp().
|
||||
Err(res.err).
|
||||
Str("file", res.name).
|
||||
Dur("time", res.duration).
|
||||
Msg("Test failed")
|
||||
|
||||
failed++
|
||||
} else {
|
||||
r.logger.Info().
|
||||
Timestamp().
|
||||
Str("file", res.name).
|
||||
Dur("time", res.duration).
|
||||
Msg("Test passed")
|
||||
|
||||
passed++
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -22,7 +24,19 @@ func New(settings Settings) *Server {
|
||||
e.Debug = false
|
||||
e.HideBanner = true
|
||||
|
||||
e.Use(func(handlerFunc echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
ctx.SetCookie(&http.Cookie{
|
||||
Name: "x-ferret",
|
||||
Value: "e2e",
|
||||
HttpOnly: false,
|
||||
})
|
||||
|
||||
return handlerFunc(ctx)
|
||||
}
|
||||
})
|
||||
e.Static("/", settings.Dir)
|
||||
e.File("/", filepath.Join(settings.Dir, "index.html"))
|
||||
|
||||
return &Server{e, settings}
|
||||
}
|
||||
|
21
e2e/tests/doc_cookie_del_d.fql
Normal file
21
e2e/tests/doc_cookie_del_d.fql
Normal file
@ -0,0 +1,21 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, {
|
||||
driver: "cdp",
|
||||
cookies: [{
|
||||
name: "x-e2e",
|
||||
value: "test"
|
||||
}, {
|
||||
name: "x-e2e-2",
|
||||
value: "test2"
|
||||
}]
|
||||
})
|
||||
|
||||
COOKIE_DEL(doc, COOKIE_GET(doc, "x-e2e"), "x-e2e-2")
|
||||
|
||||
LET cookie1 = COOKIE_GET(doc, "x-e2e")
|
||||
LET cookie2 = COOKIE_GET(doc, "x-e2e-2")
|
||||
|
||||
LET expected = "nonenone"
|
||||
LET actual = TYPENAME(cookie1) + TYPENAME(cookie2)
|
||||
|
||||
RETURN EXPECT(expected, actual)
|
10
e2e/tests/doc_cookie_get_d.fql
Normal file
10
e2e/tests/doc_cookie_get_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, {
|
||||
driver: "cdp"
|
||||
})
|
||||
|
||||
LET cookiesPath = LENGTH(doc.cookies) > 0 ? "ok" : "false"
|
||||
LET cookie = COOKIE_GET(doc, "x-ferret")
|
||||
LET expected = "ok e2e"
|
||||
|
||||
RETURN EXPECT(expected, cookiesPath + " " + cookie.value)
|
14
e2e/tests/doc_cookie_load_d.fql
Normal file
14
e2e/tests/doc_cookie_load_d.fql
Normal file
@ -0,0 +1,14 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, {
|
||||
driver: "cdp",
|
||||
cookies: [{
|
||||
name: "x-e2e",
|
||||
value: "test"
|
||||
}]
|
||||
})
|
||||
|
||||
LET cookiesPath = LENGTH(doc.cookies) > 0 ? "ok" : "false"
|
||||
LET cookie = COOKIE_GET(doc, "x-e2e")
|
||||
LET expected = "ok test"
|
||||
|
||||
RETURN EXPECT(expected, cookiesPath + " " + cookie.value)
|
14
e2e/tests/doc_cookie_set_d.fql
Normal file
14
e2e/tests/doc_cookie_set_d.fql
Normal file
@ -0,0 +1,14 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(@dynamic, {
|
||||
driver: "cdp"
|
||||
})
|
||||
|
||||
COOKIE_SET(doc, {
|
||||
name: "x-e2e",
|
||||
value: "test"
|
||||
})
|
||||
|
||||
LET cookie = COOKIE_GET(doc, "x-e2e")
|
||||
LET expected = "test"
|
||||
|
||||
RETURN EXPECT(expected, cookie.value)
|
10
e2e/tests/doc_element_exists.fql
Normal file
10
e2e/tests/doc_element_exists.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expectedP = TRUE
|
||||
LET actualP = ELEMENT_EXISTS(doc, '.section-nav')
|
||||
|
||||
LET expectedN = FALSE
|
||||
LET actualN = ELEMENT_EXISTS(doc, '.foo-bar')
|
||||
|
||||
RETURN EXPECT(expectedP + expectedN, actualP + expectedN)
|
10
e2e/tests/doc_element_exists_d.fql
Normal file
10
e2e/tests/doc_element_exists_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expectedP = TRUE
|
||||
LET actualP = ELEMENT_EXISTS(doc, '.text-center')
|
||||
|
||||
LET expectedN = FALSE
|
||||
LET actualN = ELEMENT_EXISTS(doc, '.foo-bar')
|
||||
|
||||
RETURN EXPECT(expectedP + expectedN, actualP + expectedN)
|
11
e2e/tests/doc_hover_d.fql
Normal file
11
e2e/tests/doc_hover_d.fql
Normal file
@ -0,0 +1,11 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
HOVER(doc, "#hoverable-btn")
|
||||
WAIT_ELEMENT(doc, "#hoverable-content")
|
||||
|
||||
LET output = INNER_TEXT(doc, "#hoverable-content")
|
||||
|
||||
RETURN EXPECT(output, "Lorem ipsum dolor sit amet.")
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/overview.html'
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = '<li class="toc-entry toc-h2"><a href="#containers">Containers</a></li><li class="toc-entry toc-h2"><a href="#responsive-breakpoints">Responsive breakpoints</a></li><li class="toc-entry toc-h2"><a href="#z-index">Z-index</a></li>'
|
19
e2e/tests/doc_inner_html_1_arg.fql
Normal file
19
e2e/tests/doc_inner_html_1_arg.fql
Normal file
@ -0,0 +1,19 @@
|
||||
LET url = @static + '/simple.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world</h1>
|
||||
</body>
|
||||
</html>`
|
||||
LET actual = INNER_HTML(doc)
|
||||
|
||||
LET r1 = '(\s|\")'
|
||||
LET r2 = '(\n|\s|\")'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
29
e2e/tests/doc_inner_html_1_arg_d.fql
Normal file
29
e2e/tests/doc_inner_html_1_arg_d.fql
Normal file
@ -0,0 +1,29 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
LET expected = `<!DOCTYPE html><html lang="en"><head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Ferret E2E SPA</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div id="root"><div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="/events">Events</a></li></ul></div></nav><main class="container"><div class="jumbotron" data-type="page" id="index"><div><h1>Welcome to Ferret E2E test page!</h1></div><div><p class="lead">It has several pages for testing different possibilities of the library</p></div></div></main></div></div>
|
||||
<script src="https://unpkg.com/react@16.6.1/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@16.6.1/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/history@4.7.2/umd/history.min.js"></script>
|
||||
<script src="https://unpkg.com/react-router@4.3.1/umd/react-router.js"></script>
|
||||
<script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script>
|
||||
<script src="index.js" type="module"></script>
|
||||
|
||||
|
||||
</body></html>`
|
||||
LET actual = INNER_HTML(doc)
|
||||
|
||||
LET r1 = '(\s|\")'
|
||||
LET r2 = '(\n|\s|\")'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/overview.html'
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = [
|
12
e2e/tests/doc_inner_html_all_d.fql
Normal file
12
e2e/tests/doc_inner_html_all_d.fql
Normal file
@ -0,0 +1,12 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = [
|
||||
'<h1>Welcome to Ferret E2E test page!</h1>',
|
||||
'<p class="lead">It has several pages for testing different possibilities of the library</p>'
|
||||
]
|
||||
LET actual = INNER_HTML_ALL(doc, '#root > div > main > div > *')
|
||||
|
||||
RETURN EXPECT(expected, actual)
|
10
e2e/tests/doc_inner_html_d.fql
Normal file
10
e2e/tests/doc_inner_html_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = '#root > div > main > div'
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = '<div><h1>Welcome to Ferret E2E test page!</h1></div><div><p class="lead">It has several pages for testing different possibilities of the library</p></div>'
|
||||
LET actual = INNER_HTML(doc, selector)
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, '\s', ''), REGEXP_REPLACE(TRIM(actual), '(\n|\s)', ''))
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/overview.html'
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = "Components and options for laying out your Bootstrap project, including wrapping containers, a powerful grid system, a flexible media object, and responsive utility classes."
|
10
e2e/tests/doc_inner_text_1_arg.fql
Normal file
10
e2e/tests/doc_inner_text_1_arg.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @static + '/simple.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = `Title Hello world`
|
||||
LET actual = INNER_TEXT(doc)
|
||||
|
||||
LET r1 = '(\s|\")'
|
||||
LET r2 = '(\n|\s|\")'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
17
e2e/tests/doc_inner_text_1_arg_d.fql
Normal file
17
e2e/tests/doc_inner_text_1_arg_d.fql
Normal file
@ -0,0 +1,17 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
LET expected = `Ferret E2E SPA
|
||||
Ferret
|
||||
Forms
|
||||
Navigation
|
||||
Events
|
||||
Welcome to Ferret E2E test page!
|
||||
It has several pages for testing different possibilities of the library
|
||||
`
|
||||
LET actual = INNER_TEXT(doc)
|
||||
|
||||
LET r1 = '(\n|\s)'
|
||||
LET r2 = '(\n|\s)'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/grid.html'
|
||||
LET url = @static + '/grid.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = [
|
16
e2e/tests/doc_inner_text_all_d.fql
Normal file
16
e2e/tests/doc_inner_text_all_d.fql
Normal file
@ -0,0 +1,16 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = '#root > div > main > div > *'
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = [
|
||||
'Welcome to Ferret E2E test page!',
|
||||
'It has several pages for testing different possibilities of the library'
|
||||
]
|
||||
LET actual = (
|
||||
FOR str IN INNER_TEXT_ALL(doc, selector)
|
||||
RETURN REGEXP_REPLACE(TRIM(str), '\n', '')
|
||||
)
|
||||
|
||||
RETURN EXPECT(expected, actual)
|
10
e2e/tests/doc_inner_text_d.fql
Normal file
10
e2e/tests/doc_inner_text_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = '#root > div > main > div h1'
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = 'Welcome to Ferret E2E test page!'
|
||||
LET actual = INNER_TEXT(doc, selector)
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, '\s', ''), REGEXP_REPLACE(TRIM(actual), '(\n|\s)', ''))
|
10
e2e/tests/doc_input_text_d.fql
Normal file
10
e2e/tests/doc_input_text_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET output = ELEMENT(doc, "#text_output")
|
||||
|
||||
INPUT(doc, "#text_input", "foo")
|
||||
|
||||
RETURN EXPECT(output.innerText, "foo")
|
9
e2e/tests/doc_select_multi_d.fql
Normal file
9
e2e/tests/doc_select_multi_d.fql
Normal file
@ -0,0 +1,9 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET output = ELEMENT(doc, "#multi_select_output")
|
||||
LET result = SELECT(doc, "#multi_select_input", ["1", "2", "4"])
|
||||
|
||||
RETURN EXPECT(output.innerText, "1, 2, 4") + EXPECT(JSON_STRINGIFY(result), '["1","2","4"]')
|
9
e2e/tests/doc_select_single_d.fql
Normal file
9
e2e/tests/doc_select_single_d.fql
Normal file
@ -0,0 +1,9 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET output = ELEMENT(doc, "#select_output")
|
||||
LET result = SELECT(doc, "#select_input", ["4"])
|
||||
|
||||
RETURN EXPECT(output.innerText, "4") + EXPECT(JSON_STRINGIFY(result), '["4"]')
|
9
e2e/tests/doc_wait_attr_all_d.fql
Normal file
9
e2e/tests/doc_wait_attr_all_d.fql
Normal file
@ -0,0 +1,9 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn")
|
||||
WAIT_ATTR_ALL(doc, "#wait-class-content, #wait-class-random-content", "class", "alert alert-success", 10000)
|
||||
|
||||
RETURN ""
|
18
e2e/tests/doc_wait_attr_d.fql
Normal file
18
e2e/tests/doc_wait_attr_d.fql
Normal file
@ -0,0 +1,18 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = "#wait-class-btn"
|
||||
LET attrName = "data-ferret-x"
|
||||
LET attrVal = "foobar"
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
LET el = ELEMENT(doc, selector)
|
||||
LET prev = el.attributes
|
||||
|
||||
ATTR_SET(el, attrName, attrVal)
|
||||
WAIT_ATTR(doc, selector, attrName, attrVal, 30000)
|
||||
//WAIT_ATTR(el, attrName, attrVal)
|
||||
|
||||
LET curr = el.attributes
|
||||
|
||||
RETURN prev[attrName] == NONE && curr[attrName] == attrVal ? "" : "attributes should be updated"
|
9
e2e/tests/doc_wait_class_all_d.fql
Normal file
9
e2e/tests/doc_wait_class_all_d.fql
Normal file
@ -0,0 +1,9 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn")
|
||||
WAIT_CLASS_ALL(doc, "#wait-class-content, #wait-class-random-content", "alert-success", 10000)
|
||||
|
||||
RETURN ""
|
14
e2e/tests/doc_wait_class_d.fql
Normal file
14
e2e/tests/doc_wait_class_d.fql
Normal file
@ -0,0 +1,14 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
// with fixed timeout
|
||||
CLICK(doc, "#wait-class-btn")
|
||||
WAIT_CLASS(doc, "#wait-class-content", "alert-success")
|
||||
|
||||
// with random timeout
|
||||
CLICK(doc, "#wait-class-random-btn")
|
||||
WAIT_CLASS(doc, "#wait-class-random-content", "alert-success", 10000)
|
||||
|
||||
RETURN ""
|
13
e2e/tests/doc_wait_element_d.fql
Normal file
13
e2e/tests/doc_wait_element_d.fql
Normal file
@ -0,0 +1,13 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET pageSelector = "#page-events"
|
||||
LET elemSelector = "#wait-element-content"
|
||||
LET btnSelector = "#wait-element-btn"
|
||||
|
||||
WAIT_ELEMENT(doc, pageSelector)
|
||||
|
||||
CLICK(doc, btnSelector)
|
||||
|
||||
WAIT_ELEMENT(doc, elemSelector, 10000)
|
||||
|
||||
RETURN ELEMENT_EXISTS(doc, elemSelector) ? "" : "element not found"
|
16
e2e/tests/doc_wait_no_attr_d.fql
Normal file
16
e2e/tests/doc_wait_no_attr_d.fql
Normal file
@ -0,0 +1,16 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
// with fixed timeout
|
||||
CLICK(doc, "#wait-no-class-btn")
|
||||
WAIT(1000)
|
||||
PRINT(ATTR_GET(ELEMENT(doc, "#wait-no-class-content"), "class"))
|
||||
WAIT_NO_ATTR(doc, "#wait-no-class-content", "class", "alert alert-success")
|
||||
|
||||
// with random timeout
|
||||
CLICK(doc, "#wait-no-class-random-btn")
|
||||
WAIT_NO_ATTR(doc, "#wait-no-class-random-content", "class", "alert alert-success", 10000)
|
||||
|
||||
RETURN ""
|
9
e2e/tests/doc_wait_no_class_all_d.fql
Normal file
9
e2e/tests/doc_wait_no_class_all_d.fql
Normal file
@ -0,0 +1,9 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
CLICK_ALL(doc, "#wait-no-class-btn, #wait-no-class-random-btn")
|
||||
WAIT_NO_CLASS_ALL(doc, "#wait-no-class-content, #wait-no-class-random-content", "alert-success", 10000)
|
||||
|
||||
RETURN ""
|
14
e2e/tests/doc_wait_no_class_d.fql
Normal file
14
e2e/tests/doc_wait_no_class_d.fql
Normal file
@ -0,0 +1,14 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
// with fixed timeout
|
||||
CLICK(doc, "#wait-no-class-btn")
|
||||
WAIT_NO_CLASS(doc, "#wait-no-class-content", "alert-success")
|
||||
|
||||
// with random timeout
|
||||
CLICK(doc, "#wait-no-class-random-btn")
|
||||
WAIT_NO_CLASS(doc, "#wait-no-class-random-content", "alert-success", 10000)
|
||||
|
||||
RETURN ""
|
13
e2e/tests/doc_wait_no_element_d.fql
Normal file
13
e2e/tests/doc_wait_no_element_d.fql
Normal file
@ -0,0 +1,13 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET pageSelector = "#page-events"
|
||||
LET elemSelector = "#wait-no-element-content"
|
||||
LET btnSelector = "#wait-no-element-btn"
|
||||
|
||||
WAIT_ELEMENT(doc, pageSelector)
|
||||
|
||||
CLICK(doc, btnSelector)
|
||||
|
||||
WAIT_NO_ELEMENT(doc, elemSelector, 10000)
|
||||
|
||||
RETURN ELEMENT_EXISTS(doc, elemSelector) ? "element should not be found" : ""
|
30
e2e/tests/doc_wait_no_style_all_d.fql
Normal file
30
e2e/tests/doc_wait_no_style_all_d.fql
Normal file
@ -0,0 +1,30 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = "#wait-class-btn, #wait-class-random-btn"
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
LET n = (
|
||||
FOR el IN ELEMENTS(doc, selector)
|
||||
ATTR_SET(el, "style", "color: black")
|
||||
|
||||
RETURN NONE
|
||||
)
|
||||
|
||||
WAIT_STYLE_ALL(doc, selector, "color", "black", 10000)
|
||||
|
||||
LET n2 = (
|
||||
FOR el IN ELEMENTS(doc, selector)
|
||||
ATTR_SET(el, "style", "color: red")
|
||||
|
||||
RETURN NONE
|
||||
)
|
||||
|
||||
WAIT_NO_STYLE_ALL(doc, selector, "color", "black", 10000)
|
||||
|
||||
LET results = (
|
||||
FOR el IN ELEMENTS(doc, selector)
|
||||
RETURN el.style.color
|
||||
)
|
||||
|
||||
RETURN CONCAT(results) == "redred" ? "" : "styles should be updated"
|
19
e2e/tests/doc_wait_no_style_d.fql
Normal file
19
e2e/tests/doc_wait_no_style_d.fql
Normal file
@ -0,0 +1,19 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = "#wait-class-btn"
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
LET el = ELEMENT(doc, selector)
|
||||
|
||||
ATTR_SET(el, "style", "width: 100%")
|
||||
WAIT_STYLE(doc, selector, "width", "100%")
|
||||
|
||||
LET prev = el.style
|
||||
|
||||
ATTR_SET(el, "style", "width: 50%")
|
||||
WAIT_NO_STYLE(doc, selector, "width", "100%")
|
||||
|
||||
LET curr = el.style
|
||||
|
||||
RETURN prev.width == "100%" && curr.width == "50%" ? "" : "style should be changed"
|
21
e2e/tests/doc_wait_style_all_d.fql
Normal file
21
e2e/tests/doc_wait_style_all_d.fql
Normal file
@ -0,0 +1,21 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = "#wait-class-btn, #wait-class-random-btn"
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
LET n = (
|
||||
FOR el IN ELEMENTS(doc, selector)
|
||||
ATTR_SET(el, "style", "color: black")
|
||||
|
||||
RETURN NONE
|
||||
)
|
||||
|
||||
WAIT_STYLE_ALL(doc, selector, "color", "black", 10000)
|
||||
|
||||
LET results = (
|
||||
FOR el IN ELEMENTS(doc, selector)
|
||||
RETURN el.style.color
|
||||
)
|
||||
|
||||
RETURN CONCAT(results) == "blackblack" ? "" : "styles should be updated"
|
15
e2e/tests/doc_wait_style_d.fql
Normal file
15
e2e/tests/doc_wait_style_d.fql
Normal file
@ -0,0 +1,15 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = "#wait-class-btn"
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
LET el = ELEMENT(doc, selector)
|
||||
LET prev = el.style
|
||||
|
||||
ATTR_SET(el, "style", "width: 100%")
|
||||
WAIT_STYLE(doc, selector, "width", "100%")
|
||||
|
||||
LET curr = el.style
|
||||
|
||||
RETURN prev.width == NONE && curr.width == "100%" ? "" : "style should be updated"
|
13
e2e/tests/el_attrs.fql
Normal file
13
e2e/tests/el_attrs.fql
Normal file
@ -0,0 +1,13 @@
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET el = ELEMENT(doc, "body > header > a")
|
||||
LET attrs = [
|
||||
el.attributes.class,
|
||||
el.attributes.href
|
||||
]
|
||||
|
||||
LET expected = '["navbar-brand mr-0 mr-md-2","http://getbootstrap.com/"]'
|
||||
LET actual = TO_STRING(attrs)
|
||||
|
||||
RETURN EXPECT(expected, actual)
|
13
e2e/tests/el_attrs_d.fql
Normal file
13
e2e/tests/el_attrs_d.fql
Normal file
@ -0,0 +1,13 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, { driver: "cdp" })
|
||||
|
||||
LET el = ELEMENT(doc, "#index")
|
||||
LET attrs = [
|
||||
el.attributes.class,
|
||||
el.attributes["data-type"]
|
||||
]
|
||||
|
||||
LET expected = '["jumbotron","page"]'
|
||||
LET actual = TO_STRING(attrs)
|
||||
|
||||
RETURN EXPECT(expected, actual)
|
11
e2e/tests/el_attrs_get.d.fql
Normal file
11
e2e/tests/el_attrs_get.d.fql
Normal file
@ -0,0 +1,11 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET pageSelector = "#page-events"
|
||||
LET elemSelector = "#wait-no-style-content"
|
||||
|
||||
WAIT_ELEMENT(doc, pageSelector)
|
||||
|
||||
LET el = ELEMENT(doc, elemSelector)
|
||||
LET attrs = ATTR_GET(el, "style")
|
||||
|
||||
RETURN EXPECT("display: block;", attrs.style)
|
17
e2e/tests/el_attrs_remove.d.fql
Normal file
17
e2e/tests/el_attrs_remove.d.fql
Normal file
@ -0,0 +1,17 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET pageSelector = "#page-events"
|
||||
LET elemSelector = "#wait-no-style-content"
|
||||
|
||||
WAIT_ELEMENT(doc, pageSelector)
|
||||
LET el = ELEMENT(doc, elemSelector)
|
||||
|
||||
LET prev = el.attributes.style
|
||||
|
||||
ATTR_REMOVE(el, "style")
|
||||
|
||||
WAIT(1000)
|
||||
|
||||
LET curr = el.attributes.style
|
||||
|
||||
RETURN prev == "display: block;" && curr == NONE ? "" : "expected attribute to be removed"
|
17
e2e/tests/el_attrs_set.d.fql
Normal file
17
e2e/tests/el_attrs_set.d.fql
Normal file
@ -0,0 +1,17 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET pageSelector = "#page-events"
|
||||
LET elemSelector = "#wait-no-style-content"
|
||||
|
||||
WAIT_ELEMENT(doc, pageSelector)
|
||||
|
||||
LET el = ELEMENT(doc, elemSelector)
|
||||
LET prev = el.style
|
||||
|
||||
ATTR_SET(el, "style", "color: black;")
|
||||
|
||||
WAIT(1000)
|
||||
|
||||
LET curr = el.style
|
||||
|
||||
RETURN curr.color == "black" ? "" : "styles should be updated"
|
14
e2e/tests/el_attrs_set_bulk.d.fql
Normal file
14
e2e/tests/el_attrs_set_bulk.d.fql
Normal file
@ -0,0 +1,14 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET pageSelector = "#page-events"
|
||||
LET elemSelector = "#wait-no-style-content"
|
||||
|
||||
WAIT_ELEMENT(doc, pageSelector)
|
||||
|
||||
LET el = ELEMENT(doc, elemSelector)
|
||||
|
||||
ATTR_SET(el, { style: "color: black;", "data-ferret-x": "test" })
|
||||
|
||||
WAIT(1000)
|
||||
|
||||
RETURN el.style.color == "black" && el.attributes["data-ferret-x"] == "test" ? "" : "styles should be updated"
|
12
e2e/tests/el_element_exists.fql
Normal file
12
e2e/tests/el_element_exists.fql
Normal file
@ -0,0 +1,12 @@
|
||||
LET url = @static + '/value.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET el = ELEMENT(doc, "#listings_table")
|
||||
|
||||
LET expectedP = TRUE
|
||||
LET actualP = ELEMENT_EXISTS(el, '.odd')
|
||||
|
||||
LET expectedN = FALSE
|
||||
LET actualN = ELEMENT_EXISTS(el, '.foo-bar')
|
||||
|
||||
RETURN EXPECT(expectedP + expectedN, actualP + expectedN)
|
12
e2e/tests/el_element_exists_d.fql
Normal file
12
e2e/tests/el_element_exists_d.fql
Normal file
@ -0,0 +1,12 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET el = ELEMENT(doc, "#root")
|
||||
|
||||
LET expectedP = TRUE
|
||||
LET actualP = ELEMENT_EXISTS(el, '.jumbotron')
|
||||
|
||||
LET expectedN = FALSE
|
||||
LET actualN = ELEMENT_EXISTS(el, '.foo-bar')
|
||||
|
||||
RETURN EXPECT(expectedP + expectedN, actualP + expectedN)
|
13
e2e/tests/el_hover_d.fql
Normal file
13
e2e/tests/el_hover_d.fql
Normal file
@ -0,0 +1,13 @@
|
||||
LET url = @dynamic + "?redirect=/events"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#page-events")
|
||||
|
||||
LET input = ELEMENT(doc, "#hoverable-btn")
|
||||
|
||||
HOVER(input)
|
||||
WAIT_ELEMENT(doc, "#hoverable-content")
|
||||
|
||||
LET output = ELEMENT(doc, "#hoverable-content")
|
||||
|
||||
RETURN EXPECT(output.innerText, "Lorem ipsum dolor sit amet.")
|
11
e2e/tests/el_inner_html_1_arg.fql
Normal file
11
e2e/tests/el_inner_html_1_arg.fql
Normal file
@ -0,0 +1,11 @@
|
||||
LET url = @static + '/simple.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
LET el = ELEMENT(doc, "body")
|
||||
|
||||
LET expected = `<h1>Hello world</h1>`
|
||||
LET actual = INNER_HTML(el)
|
||||
|
||||
LET r1 = '(\s|\")'
|
||||
LET r2 = '(\n|\s|\")'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
11
e2e/tests/el_inner_html_1_arg_d.fql
Normal file
11
e2e/tests/el_inner_html_1_arg_d.fql
Normal file
@ -0,0 +1,11 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET el = ELEMENT(doc, "#root")
|
||||
|
||||
LET expected = `<div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="/events">Events</a></li></ul></div></nav><main class="container"><div class="jumbotron" data-type="page" id="index"><div><h1>Welcome to Ferret E2E test page!</h1></div><div><p class="lead">It has several pages for testing different possibilities of the library</p></div></div></main></div>`
|
||||
LET actual = INNER_HTML(el)
|
||||
|
||||
LET r1 = '(\s|\")'
|
||||
LET r2 = '(\n|\s|\")'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
11
e2e/tests/el_inner_text_1_arg.fql
Normal file
11
e2e/tests/el_inner_text_1_arg.fql
Normal file
@ -0,0 +1,11 @@
|
||||
LET url = @static + '/simple.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
LET el = ELEMENT(doc, "body")
|
||||
|
||||
LET expected = `Hello world`
|
||||
LET actual = INNER_TEXT(el)
|
||||
|
||||
LET r1 = '(\s|\")'
|
||||
LET r2 = '(\n|\s|\")'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
14
e2e/tests/el_inner_text_1_arg_d.fql
Normal file
14
e2e/tests/el_inner_text_1_arg_d.fql
Normal file
@ -0,0 +1,14 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET el = ELEMENT(doc, ".jumbotron")
|
||||
|
||||
LET expected = `
|
||||
Welcome to Ferret E2E test page!
|
||||
It has several pages for testing different possibilities of the library
|
||||
`
|
||||
LET actual = INNER_TEXT(el)
|
||||
|
||||
LET r1 = '(\n|\s)'
|
||||
LET r2 = '(\n|\s)'
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))
|
11
e2e/tests/el_input_text_d.fql
Normal file
11
e2e/tests/el_input_text_d.fql
Normal file
@ -0,0 +1,11 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET input = ELEMENT(doc, "#text_input")
|
||||
LET output = ELEMENT(doc, "#text_output")
|
||||
|
||||
INPUT(input, "foo")
|
||||
|
||||
RETURN EXPECT(output.innerText, "foo")
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user