1
0
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:
3timeslazy 2019-04-10 13:20:20 +03:00
parent 9f7aa5132d
commit 9655efe874
489 changed files with 20577 additions and 7701 deletions

21
.codecov.yml Normal file
View 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

View File

@ -12,9 +12,7 @@ builds:
- darwin
- windows
goarch:
- 386
- amd64
- arm
- arm64
archive:

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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"

View File

@ -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
View File

@ -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
View 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
}

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -18,6 +18,12 @@ type (
}
)
const (
goosWindows = "windows"
goosLinux = "linux"
goosDarwin = "darwin"
)
func WithoutDefaultArgs() Option {
return func(opts *Options) {
opts.ignoreDefaultArgs = true

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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(&#39;send&#39;, &#39;event&#39;, &#39;Navbar&#39;, &#39;Community links&#39;, &#39;Bootstrap&#39;);">Home</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="http://getbootstrap.com/docs/4.1/getting-started/introduction/" onclick="ga(&#39;send&#39;, &#39;event&#39;, &#39;Navbar&#39;, &#39;Community links&#39;, &#39;Docs&#39;);">Documentation</a>
</li>
<li class="nav-item">
<a class="nav-link " href="http://getbootstrap.com/docs/4.1/examples/" onclick="ga(&#39;send&#39;, &#39;event&#39;, &#39;Navbar&#39;, &#39;Community links&#39;, &#39;Examples&#39;);">Examples</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://themes.getbootstrap.com/" onclick="ga(&#39;send&#39;, &#39;event&#39;, &#39;Navbar&#39;, &#39;Community links&#39;, &#39;Themes&#39;);" target="_blank" rel="noopener">Themes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://expo.getbootstrap.com/" onclick="ga(&#39;send&#39;, &#39;event&#39;, &#39;Navbar&#39;, &#39;Community links&#39;, &#39;Expo&#39;);" target="_blank" rel="noopener">Expo</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://blog.getbootstrap.com/" onclick="ga(&#39;send&#39;, &#39;event&#39;, &#39;Navbar&#39;, &#39;Community links&#39;, &#39;Blog&#39;);" 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, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; 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 &amp; 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&amp;utm_medium=ad_via_link&amp;utm_campaign=in_unit&amp;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">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
<span class="c">&lt;!-- Content here --&gt;</span>
<span class="nt">&lt;/div&gt;</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">&lt;div</span> <span class="na">class=</span><span class="s">"container-fluid"</span><span class="nt">&gt;</span>
...
<span class="nt">&lt;/div&gt;</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>

View 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
])
)
}

View 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)
])
}

View 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)
]);
}
}

View 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."
])
])
])
]);
}
}

View 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)
]);
}
}

View 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,
})
]),
])
])
}
}

View 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
)
]),
])
}
}

View 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")
)
])
}

View File

@ -0,0 +1,4 @@
/* Show it's not fixed to the top */
body {
min-height: 75rem;
}

View 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>

View 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")
);

View 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('&') : '';
}

View 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;
}

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View File

@ -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
}

View File

@ -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++
}

View File

@ -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}
}

View 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)

View 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)

View 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)

View 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)

View 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)

View 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
View 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.")

View File

@ -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>'

View 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, ''))

View 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, ''))

View File

@ -1,4 +1,4 @@
LET url = @server + '/bootstrap/overview.html'
LET url = @static + '/overview.html'
LET doc = DOCUMENT(url)
LET expected = [

View 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)

View 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)', ''))

View File

@ -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."

View 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, ''))

View 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, ''))

View File

@ -1,4 +1,4 @@
LET url = @server + '/bootstrap/grid.html'
LET url = @static + '/grid.html'
LET doc = DOCUMENT(url)
LET expected = [

View 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)

View 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)', ''))

View 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")

View 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"]')

View 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"]')

View 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 ""

View 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"

View 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 ""

View 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 ""

View 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"

View 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 ""

View 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 ""

View 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 ""

View 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" : ""

View 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"

View 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"

View 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"

View 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
View 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
View 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)

View 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)

View 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"

View 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"

View 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"

View 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)

View 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
View 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.")

View 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, ''))

View 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, ''))

View 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, ''))

View 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, ''))

View 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