Skip to content

Commit bb28172

Browse files
committed
add browser tests
- this is the first set of browser tests - currently with firefox - run it per: (cd tests; nix-shell --run 'python -m unittest')
1 parent c8277b6 commit bb28172

File tree

10 files changed

+268
-9
lines changed

10 files changed

+268
-9
lines changed

tests/fs.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# simple file-system utils
2+
from typing import List
3+
import os
4+
5+
def createTestFile(path: str, lines: List[str]) -> None:
6+
with open(path, "w") as fh:
7+
for line in lines:
8+
fh.write(line + "\n")
9+
10+
11+
def exists(path: str) -> bool:
12+
return os.path.isfile(path)

tests/page.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# the web-page actions
2+
from typing import Any, cast
3+
from selenium import webdriver # type: ignore
4+
5+
class Page:
6+
7+
def __init__(self, headless: bool = True) -> None:
8+
options = webdriver.FirefoxOptions()
9+
if headless:
10+
options.add_argument('--headless')
11+
self.wd = webdriver.Firefox(options = options)
12+
self.wd.implicitly_wait(10) # in seconds
13+
self.wd.get("http://localhost:12345")
14+
15+
16+
def title(self) -> str:
17+
return cast(str, self.wd.title)
18+
19+
20+
def selectView(self, name: str) -> None:
21+
self.wd.find_element_by_link_text(name).click()
22+
23+
24+
def selectDataset(self, name: str) -> None:
25+
path = "//td[contains(.,'{}')]".format(name)
26+
self.findByXPath(path).click()
27+
28+
29+
def createSnapshot(self, name: str) -> None:
30+
self.findById("create-snapshot").click()
31+
self.findById("snapshot-name-template").clear()
32+
self.findById("snapshot-name-template").send_keys(name)
33+
self.findById("confirm-btn-ok").click()
34+
35+
36+
def destroySnapshot(self, name: str) -> None:
37+
self.findById("snapshot-actions").click()
38+
self.findById("destroy-" + name).click()
39+
self.findById("confirm-btn-ok").click()
40+
41+
42+
def renameSnapshot(self, name: str, newName: str) -> None:
43+
self.findById("snapshot-actions").click()
44+
self.findById("rename-" + name).click()
45+
self.findById("snapshot-name-template").clear()
46+
self.findById("snapshot-name-template").send_keys(newName)
47+
self.findById("confirm-btn-ok").click()
48+
49+
50+
def cloneSnapshot(self, snapName: str, dsName: str) -> None:
51+
self.findById("snapshot-actions").click()
52+
self.findById("clone-" + snapName).click()
53+
self.findById("fs-name").clear()
54+
self.findById("fs-name").send_keys(dsName)
55+
self.findById("confirm-btn-ok").click()
56+
57+
58+
def rollbackSnapshot(self, name: str) -> None:
59+
self.findById("snapshot-actions").click()
60+
self.findById("rollback-" + name).click()
61+
self.findById("confirm-btn-ok").click()
62+
63+
64+
def alertText(self) -> str:
65+
return cast(str, self.findByCSS(".alert").text)
66+
67+
68+
def closeAlert(self) -> None:
69+
self.findByCSS(".alert > .close").click()
70+
71+
72+
def findById(self, id: str) -> Any:
73+
return self.wd.find_element_by_id(id)
74+
75+
76+
def findByCSS(self, sel: str) -> Any:
77+
return self.wd.find_element_by_css_selector(sel)
78+
79+
80+
def findByXPath(self, path: str) -> Any:
81+
return self.wd.find_element_by_xpath(path)
82+
83+
84+
def close(self) -> None:
85+
self.wd.quit()

tests/shell.nix

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
with (import <nixpkgs> {});
2+
mkShell {
3+
buildInputs = [
4+
python3Packages.selenium
5+
mypy
6+
firefox
7+
];
8+
}

tests/tests.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# the test suite - run it per `python -m unittest`
2+
import unittest
3+
from typing import Any
4+
from selenium import webdriver # type: ignore
5+
import sys
6+
7+
import fs
8+
import zfs
9+
from page import Page
10+
11+
class Tests(unittest.TestCase):
12+
mountpoint: str
13+
pool = "zguest"
14+
15+
@classmethod
16+
def setUpClass(cls) -> None:
17+
cls.mountpoint = zfs.createDataset(cls.pool, "zsd-browser-test")
18+
19+
20+
def setUp(self) -> None:
21+
self.page = Page(headless = True)
22+
self.assertIn("ZFS-Snap-Diff", self.page.title())
23+
24+
25+
def testActualFileContent(self) -> None:
26+
fs.createTestFile(self.mountpoint + "/file.txt",
27+
["firstline", "secondline", "thirdline"]
28+
)
29+
30+
self.page.selectView("Browse filesystem")
31+
self.page.selectDataset("zsd-browser-test")
32+
self.page.findByXPath("//td[contains(.,'file.txt')]").click()
33+
self.assertIn("Current content of file.txt", self.page.findById("file-actions-header").text)
34+
self.assertIn("firstline\nsecondline\nthirdline", self.page.findById("file-actions-body").text)
35+
36+
37+
def testCreateSnapshotInBrowseFilesystem(self) -> None:
38+
self.page.selectView("Browse filesystem")
39+
self.page.selectDataset("zsd-browser-test")
40+
self.page.createSnapshot("create-snapshot-in-browse-filesystem")
41+
self.assertIn("@create-snapshot-in-browse-filesystem' created", self.page.alertText())
42+
43+
44+
def testCreateSnapshotInBrowseSnapshots(self) -> None:
45+
self.page.selectView("Browse snapshots")
46+
self.page.selectDataset("zsd-browser-test")
47+
self.page.createSnapshot("create-snapshot-in-browse-snapshots")
48+
self.assertIn("@create-snapshot-in-browse-snapshots' created", self.page.alertText())
49+
50+
51+
def testDestroySnapshot(self) -> None:
52+
self.page.selectView("Browse snapshots")
53+
self.page.selectDataset("zsd-browser-test")
54+
55+
# create snapshot
56+
self.page.createSnapshot("destroy-snapshot")
57+
self.page.closeAlert()
58+
59+
# destroy snapshot
60+
self.page.destroySnapshot("destroy-snapshot")
61+
self.assertIn("Snapshot 'destroy-snapshot' destroyed", self.page.alertText())
62+
self.page.closeAlert()
63+
64+
65+
def testRenameSnapshot(self) -> None:
66+
self.page.selectView("Browse snapshots")
67+
self.page.selectDataset("zsd-browser-test")
68+
69+
# create snapshot
70+
self.page.createSnapshot("rename-snapshot")
71+
self.page.closeAlert()
72+
73+
# rename snapshot
74+
self.page.renameSnapshot("rename-snapshot", "snapshot-rename")
75+
self.assertIn("Snapshot 'rename-snapshot' renamed to 'snapshot-rename'", self.page.alertText())
76+
self.page.closeAlert()
77+
78+
79+
def testCloneSnapshot(self) -> None:
80+
self.page.selectView("Browse snapshots")
81+
self.page.selectDataset("zsd-browser-test")
82+
83+
# create snapshot
84+
self.page.createSnapshot("clone-snapshot")
85+
self.page.closeAlert()
86+
87+
# clone snapshot
88+
self.page.cloneSnapshot("clone-snapshot", "cloned")
89+
self.assertIn("Snapshot 'clone-snapshot' cloned to '"+self.pool+"/cloned'", self.page.alertText())
90+
self.page.closeAlert()
91+
92+
93+
def testRollbackSnapshot(self) -> None:
94+
self.page.selectView("Browse snapshots")
95+
self.page.selectDataset("zsd-browser-test")
96+
97+
# create snapshot
98+
self.page.createSnapshot("rollback-snapshot")
99+
self.assertIn("@rollback-snapshot' created", self.page.alertText())
100+
self.page.closeAlert()
101+
102+
# create a file
103+
fs.createTestFile(self.mountpoint + "/rollback-test.txt", ["dummy"])
104+
self.assertTrue(fs.exists(self.mountpoint + "/rollback-test.txt"))
105+
106+
# rollback
107+
self.page.rollbackSnapshot("rollback-snapshot")
108+
self.assertIn("Snapshot 'rollback-snapshot' rolled back", self.page.alertText())
109+
self.assertFalse(fs.exists(self.mountpoint + "/rollback-test.txt"))
110+
111+
112+
def tearDown(self) -> None:
113+
self.page.close()
114+
115+
116+
@classmethod
117+
def tearDownClass(cls) -> None:
118+
zfs.destroyDataset(cls.pool, "zsd-browser-test")

tests/zfs.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Any, Union, List
2+
import subprocess
3+
import os
4+
5+
def createDataset(pool: str, name: str) -> str:
6+
mountpoint = "/tmp/{}".format(name)
7+
args = "create -o mountpoint={} {}/{}".format(mountpoint, pool, name)
8+
zfs(args)
9+
10+
# fix permission
11+
username = os.getlogin()
12+
subprocess.run(["sudo", "chown", username, mountpoint])
13+
14+
return mountpoint
15+
16+
17+
def destroyDataset(pool: str, name: str) -> None:
18+
zfs("destroy -R {}/{}".format(pool, name))
19+
20+
21+
def zfs(args: Union[str, List[str]]) -> None:
22+
if isinstance(args, str):
23+
args = args.split()
24+
subprocess.run(["sudo", "zfs"] + args)
25+

webapp/src/ZSD/Components/Confirm.purs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ confirm =
4343
$ fragment
4444
[ R.button
4545
{ className: "btn btn-secondary"
46+
, id: "confirm-btn-cancel"
4647
, onClick: capture_ props.onCancel
4748
, children: [ R.text "Cancel" ]
4849
}
4950
, R.button
5051
{ className: "btn btn-primary"
52+
, id: "confirm-btn-ok"
5153
, onClick: capture_ props.onOk
5254
, children: [ R.text "Ok" ]
5355
}

webapp/src/ZSD/Fragments/DatasetSelector.purs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ datasetSelector = make component { initialState, didMount, render }
4949
R.span
5050
{ className: "float-right fas fa-camera pointer p-1"
5151
, title: "Create a snapshot for " <> ds.name
52+
, id: "create-snapshot"
5253
, onClick:
5354
capture_
5455
$ self.setState

webapp/src/ZSD/Fragments/FileActions.purs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
module ZSD.Fragments.FileActions where
22

33
import Prelude
4+
45
import Data.Array as A
56
import Data.Either (either)
67
import Data.Monoid (guard)
78
import Data.Newtype (unwrap)
9+
import Data.String as S
810
import Effect (Effect)
911
import Effect.Aff (launchAff_)
1012
import Effect.Class (liftEffect)
@@ -16,7 +18,6 @@ import Web.HTML (window)
1618
import Web.HTML.Location (assign)
1719
import Web.HTML.Window (location)
1820
import ZSD.Components.ActionButton (actionButton)
19-
import ZSD.Views.Messages as Messages
2021
import ZSD.Fragments.FileAction.ViewDiff (viewDiff)
2122
import ZSD.Fragments.FileActions.ViewBlob (viewBlob)
2223
import ZSD.Fragments.FileActions.ViewText (viewText)
@@ -27,6 +28,7 @@ import ZSD.Model.FileVersion as FileVersion
2728
import ZSD.Model.MimeType (MimeType(..))
2829
import ZSD.Model.MimeType as MimeType
2930
import ZSD.Utils.Ops (checkAny)
31+
import ZSD.Views.Messages as Messages
3032

3133
type Props
3234
= { file :: FH, version :: FileVersion }
@@ -134,6 +136,7 @@ fileAction = make component { initialState, render, didMount, didUpdate }
134136
, children:
135137
[ R.div
136138
{ className: "card-header"
139+
, id: "file-actions-header"
137140
, children:
138141
case self.props.version of
139142
CurrentVersion current ->
@@ -150,6 +153,7 @@ fileAction = make component { initialState, render, didMount, didUpdate }
150153
}
151154
, R.div
152155
{ className: "card-body"
156+
, id: "file-actions-body"
153157
, children: [ self.state.view ]
154158
}
155159
]
@@ -162,6 +166,7 @@ fileAction = make component { initialState, render, didMount, didUpdate }
162166
{ className:
163167
"btn btn-secondary" <> guard (not enabled) " disabled"
164168
<> guard (self.state.cmd == action) " active"
169+
, id: "btn-" <> S.toLower title
165170
, onClick:
166171
capture_
167172
$ if (enabled) then

webapp/src/ZSD/Fragments/SnapshotNameForm.purs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,14 @@ snapshotNameForm = make component { initialState, didMount, render }
7676
, children:
7777
[ div "form-group row"
7878
$ R.label
79-
{ htmlFor: "template"
79+
{ htmlFor: "snapshot-name-template"
8080
, children: [ R.text "Snapshot name template" ]
8181
}
8282
<> R.input
8383
{ className:
8484
"form-control"
8585
<> guard (isJust self.state.error) " is-invalid"
86-
, id: "template"
86+
, id: "snapshot-name-template"
8787
, autoFocus: true
8888
, placeholder: "Snapshot name template"
8989
, onChange: capture targetValue (fromMaybe "" >>> ConvertTemplate >>> update self)

webapp/src/ZSD/Views/BrowseSnapshots/SnapshotSelector.purs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
module ZSD.Views.BrowseSnapshots.SnapshotSelector where
22

33
import Prelude
4+
45
import Data.Array as A
56
import Data.Either (either)
67
import Data.Foldable (foldMap)
78
import Data.Maybe (Maybe(..), fromMaybe, maybe)
89
import Data.Monoid (guard)
10+
import Data.String as S
911
import Data.Tuple (Tuple(..))
1012
import Effect (Effect)
1113
import Effect.Aff (launchAff_)
@@ -195,25 +197,26 @@ snapshotSelector = make component { initialState, didMount, didUpdate, render }
195197
[ R.div
196198
{ className: "mx-auto"
197199
, style: R.css { width: "10px" }
198-
, children: [ R.span { className: "fas fa-ellipsis-v" } ]
200+
, children: [ R.span { className: "fas fa-ellipsis-v", id: "snapshot-actions" } ]
199201
}
200202
]
201203
}
202204
, R.div
203205
{ className: "dropdown-menu"
204206
, children:
205-
[ dropdownItem self "Rename snapshot" $ RenameSnapshot snap
206-
, dropdownItem self "Destroy snapshot" $ DestroySnapshot snap
207-
, dropdownItem self "Clone" $ CloneSnapshot snap
208-
, dropdownItem self "Rollback" $ RollbackSnapshot snap
207+
[ dropdownItem self "Rename" snap $ RenameSnapshot snap
208+
, dropdownItem self "Destroy" snap $ DestroySnapshot snap
209+
, dropdownItem self "Clone" snap $ CloneSnapshot snap
210+
, dropdownItem self "Rollback" snap $ RollbackSnapshot snap
209211
]
210212
}
211213
]
212214
}
213215

214-
dropdownItem self name cmd =
216+
dropdownItem self name snap cmd =
215217
R.button
216218
{ className: "dropdown-item"
219+
, id: (S.toLower name) <> "-" <> snap.name
217220
, onClick: capture_ $ update self cmd
218221
, children: [ R.text name ]
219222
}

0 commit comments

Comments
 (0)