diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ddbacc4c..a6c389e4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -16,7 +16,6 @@ - @@ -35,7 +34,6 @@ - diff --git a/.travis.yml b/.travis.yml index d62b80d4..466ae517 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ before_install: - sudo rm /dev/random - sudo mknod /dev/random c 1 9 # for gui tests + - export DBUS_SESSION_BUS_ADDRESS=/dev/null - export CHROME_BIN=/usr/bin/google-chrome - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start @@ -33,9 +34,9 @@ before_install: install: #install and run tests and run style checking - - mvn install -P travis + - mvn clean install -P travis - cd ui-testing - - mvn install -DskipTests + - mvn clean install -DskipTests - cd .. script: @@ -67,8 +68,7 @@ script: - sleep 30 - cd ../ui-testing - - mvn test -P travis & - - sleep 30 + - mvn test -P travis notifications: slack: qreal-web:sT5qgA4qZZ9eyLI0yy2Mp81E diff --git a/Travis/callTomcat.sh b/Travis/callTomcat.sh new file mode 100755 index 00000000..bfc51400 --- /dev/null +++ b/Travis/callTomcat.sh @@ -0,0 +1,47 @@ +#!/bin/bash +iter=1 +all=120 +until [ "$(curl --silent --show-error --connect-timeout 1 -I http://localhost:"${1:-"8080"}"/auth | grep '302 Found')" != "" ]; +do + if [ "$iter" -lt "$all" ] + then + echo "--- sleeping for 10 seconds" + sleep 10 + let iter=$iter+1 + else + echo "Server didn't return 302 found for long time" + exit 1 + fi +done +echo "auth-service found" +iter=1 +all=120 +until [ "$(curl --silent --show-error --connect-timeout 1 -I http://localhost:"${2:-"8082"}"/dashboard | grep '302 Found')" != "" ]; +do + if [ "$iter" -lt "$all" ] + then + echo "--- sleeping for 10 seconds" + sleep 10 + let iter=$iter+1 + else + echo "Server didn't return 302 found for long time" + exit 1 + fi +done +echo "dashboard-service found" +iter=1 +all=120 +until [ "$(curl --silent --show-error --connect-timeout 1 -I http://localhost:"${3:-"8081"}"/editor | grep '302 Found')" != "" ]; +do + if [ "$iter" -lt "$all" ] + then + echo "--- sleeping for 10 seconds" + sleep 10 + let iter=$iter+1 + else + echo "Server didn't return 302 found for long time" + exit 1 + fi +done +echo "editor-service found" +exit 0 diff --git a/Travis/checkstyle/checkstyle.xml b/Travis/checkstyle/checkstyle.xml index e51a03d2..d4fa676d 100644 --- a/Travis/checkstyle/checkstyle.xml +++ b/Travis/checkstyle/checkstyle.xml @@ -29,7 +29,9 @@ - + + + @@ -68,9 +70,9 @@ - + value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/> + @@ -90,60 +92,60 @@ + value="Package name ''{0}'' must match pattern ''{1}''."/> + value="Type name ''{0}'' must match pattern ''{1}''."/> + value="Member name ''{0}'' must match pattern ''{1}''."/> + value="Parameter name ''{0}'' must match pattern ''{1}''."/> + value="Local variable name ''{0}'' must match pattern ''{1}''."/> + value="Class type name ''{0}'' must match pattern ''{1}''."/> + value="Method type name ''{0}'' must match pattern ''{1}''."/> + value="Interface type name ''{0}'' must match pattern ''{1}''."/> + value="Method name ''{0}'' must match pattern ''{1}''."/> - - - + value="GenericWhitespace ''{0}'' is followed by whitespace."/> + + + @@ -170,7 +172,7 @@ - + @@ -187,5 +189,6 @@ + diff --git a/Travis/pmd/pmd-ruleset.xml b/Travis/pmd/pmd-ruleset.xml index bef3725e..7e3a7df8 100644 --- a/Travis/pmd/pmd-ruleset.xml +++ b/Travis/pmd/pmd-ruleset.xml @@ -11,7 +11,9 @@ - + + + diff --git a/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/dao/DiagramDaoImpl.java b/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/dao/DiagramDaoImpl.java index ace10fad..e949da8b 100644 --- a/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/dao/DiagramDaoImpl.java +++ b/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/dao/DiagramDaoImpl.java @@ -179,6 +179,10 @@ public void deleteFolder(Long folderId) throws AbortedException { DiagramDaoImpl.class.getName()); } Folder folder = (Folder) session.get(Folder.class, folderId); + for (Folder child : folder.getChildrenFolders()) { + deleteFolder(child.getId()); + } + folder.remove(); session.delete(folder); logger.trace("deleteFolder() successfully deleted a folder with id {}", folderId); diff --git a/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/model/Folder.java b/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/model/Folder.java index b3035249..f7f6b9f6 100644 --- a/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/model/Folder.java +++ b/db-services/db-diagram-service/src/main/java/com/qreal/wmp/db/diagram/model/Folder.java @@ -19,6 +19,8 @@ @ToString(exclude = "parentFolders") public class Folder implements Serializable { + private static final String ROOT_FOLDER = "root"; + @Id @Column(name = "folder_id") @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -35,10 +37,10 @@ public class Folder implements Serializable { @Transient private Long folderParentId; - @ManyToMany(cascade = CascadeType.REMOVE, fetch = FetchType.EAGER, mappedBy = "childrenFolders") + @ManyToMany(fetch = FetchType.EAGER, mappedBy = "childrenFolders") private Set parentFolders = new HashSet<>(); - @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, fetch = FetchType.EAGER) @JoinTable(name = "folders_folders", joinColumns = {@JoinColumn(name = "parent_id")}, inverseJoinColumns = {@JoinColumn(name = "child_id")}) private Set childrenFolders = new HashSet<>(); @@ -95,7 +97,68 @@ public Folder(TFolder tFolder) { diagrams = tFolder.getDiagrams().stream().map(Diagram::new).collect(Collectors.toSet()); } } - + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public void setFolderName(String folderName) { + this.folderName = folderName; + } + + public void remove() { + parentFolders.forEach(x -> x.removeChild(id)); + parentFolders.clear(); + } + + public Set getChildrenFolders() { + return childrenFolders; + } + + public Set getOwners() { + return owners; + } + + public void setOwners(Set owners) { + this.owners = owners; + } + + public Long getFolderParentId() { + return folderParentId; + } + + public void setFolderParentId(Long folderParentId) { + this.folderParentId = folderParentId; + } + + public Set getParentFolders() { + return parentFolders; + } + + public void setParentFolders(Set parentFolders) { + this.parentFolders = parentFolders; + } + + public void setChildrenFolders(Set childrenFolders) { + this.childrenFolders = childrenFolders; + } + + public Set getDiagrams() { + return diagrams; + } + + public void setDiagrams(Set diagrams) { + this.diagrams = diagrams; + } + + public String getFolderName() { + return folderName; + } + /** Converter from Folder to Thrift TFolder.*/ public TFolder toTFolder(final String username) { TFolder tFolder = new TFolder(); @@ -134,5 +197,9 @@ private void setFolderParentId(String username, TFolder tFolder) { tFolder.setFolderParentId(folderParentId); } } + + private void removeChild(long childId) { + childrenFolders.stream().filter(x -> x.id == childId).findFirst().ifPresent(x -> childrenFolders.remove(x)); + } } diff --git a/editor-core/src/main/webapp/app/core/editorCore/controller/SceneController.ts b/editor-core/src/main/webapp/app/core/editorCore/controller/SceneController.ts index 669fe7ab..cd0ce699 100644 --- a/editor-core/src/main/webapp/app/core/editorCore/controller/SceneController.ts +++ b/editor-core/src/main/webapp/app/core/editorCore/controller/SceneController.ts @@ -10,6 +10,7 @@ import {DiagramElement} from "../model/DiagramElement"; import {SubprogramNode} from "../model/SubprogramNode"; import {Property} from "../model/Property"; import {NodeType} from "../model/NodeType"; +import {Scroller, Direction} from "../model/Scroller"; import {DiagramElementListener} from "./DiagramElementListener"; import {SceneCommandFactory} from "../model/commands/SceneCommandFactory"; import {DiagramEditorController} from "./DiagramEditorController"; @@ -21,8 +22,10 @@ export class SceneController { private currentElement: DiagramElement; private clickFlag : boolean; private rightClickFlag : boolean; + private scroller : Scroller; private undoRedoController: UndoRedoController; private lastCellMouseDownPosition: {x: number, y: number}; + private lastCellScrollPosition: {x: number, y: number}; private lastCellMouseDownSize: {width: number, height: number}; private paperCommandFactory: SceneCommandFactory; private contextMenuId = "scene-context-menu"; @@ -34,8 +37,10 @@ export class SceneController { this.paperCommandFactory = new SceneCommandFactory(this); this.clickFlag = false; this.rightClickFlag = false; + this.scroller = new Scroller(); this.lastCellMouseDownPosition = { x: 0, y: 0 }; this.lastCellMouseDownSize = { width: 0, height: 0 }; + this.lastCellScrollPosition = { x: 0, y: 0 }; this.scene.on('cell:pointerdown', (cellView, event, x, y): void => { this.cellPointerdownListener(cellView, event, x, y); @@ -52,10 +57,12 @@ export class SceneController { }); this.diagramEditorController.getGraph().on('change:position', (cell) => { - if (!this.rightClickFlag) { - return; + if (this.scroller.getScroll()) { + cell.set('position', this.lastCellScrollPosition); + } + if (this.rightClickFlag) { + cell.set('position', cell.previous('position')); } - cell.set('position', cell.previous('position')); }); this.initDropPaletteElementListener(); @@ -247,7 +254,7 @@ export class SceneController { this.changeCurrentElement(element); if (this.scene.getNodeById(cellView.model.id) && event.button == MouseButton.left) { - var node:DiagramNode = this.scene.getNodeById(cellView.model.id); + var node: DiagramNode = this.scene.getNodeById(cellView.model.id); this.lastCellMouseDownPosition.x = node.getX(); this.lastCellMouseDownPosition.y = node.getY(); var bbox = cellView.getBBox(); @@ -270,6 +277,7 @@ export class SceneController { }); } else if (event.button == MouseButton.left){ + this.borderUnCrossed(); var node: DiagramNode = this.scene.getNodeById(cellView.model.id); if (node) { if (node.isResizing()) { @@ -300,6 +308,11 @@ export class SceneController { } private cellPointermoveListener(cellView, event, x, y): void { + var element: DiagramElement = this.scene.getNodeById(cellView.model.id) || + this.scene.getLinkById(cellView.model.id); + if (element instanceof DefaultDiagramNode) { + this.checkBorder(element, cellView, event) + } this.clickFlag = false; var node: DiagramNode = this.scene.getNodeById(cellView.model.id); if (node) { @@ -407,6 +420,121 @@ export class SceneController { }); } + private checkBorder(element: DiagramElement, cellView, event) : void { + var sceneWrapper: HTMLDivElement = $(".scene-wrapper")[0]; + var boundingBox: any = sceneWrapper.getBoundingClientRect(); + + var node = this.scene.getNodeById(cellView.model.id); + this.borderUnCrossed(); + if (event.pageX + this.scene.getGridSize() * this.scene.getZoom() >= boundingBox.right) { + this.scroller.setDirection(Direction.Right); + this.borderCrossed(node, event, cellView); + } else if (event.pageX - this.scene.getGridSize() * this.scene.getZoom() <= boundingBox.left) { + this.scroller.setDirection(Direction.Left); + this.borderCrossed(node, event, cellView); + } else if (event.pageY + this.scene.getGridSize() * this.scene.getZoom() >= boundingBox.bottom) { + this.scroller.setDirection(Direction.Down); + this.borderCrossed(node, event, cellView); + } else if (event.pageY - this.scene.getGridSize() * this.scene.getZoom() <= boundingBox.top) { + this.scroller.setDirection(Direction.Up); + this.borderCrossed(node, event, cellView); + } + this.updateLastCellScrollPosition(event); + } + + private borderCrossed(node: DiagramNode, event, cellView): void { + this.scroller.setScroll(true); + var that = this; + switch (this.scroller.getDirection()) { + case Direction.Right: { + this.scroller.setIntervalId(setInterval(() => that.scrollRight(node, event, cellView), 150)); + break; + } + case Direction.Left: { + this.scroller.setIntervalId(setInterval(() => that.scrollLeft(node, event, cellView), 150)); + break; + } + case Direction.Down: { + this.scroller.setIntervalId(setInterval(() => that.scrollBottom(node, event, cellView), 150)); + break; + } + case Direction.Up: { + this.scroller.setIntervalId(setInterval(() => that.scrollTop(node, event, cellView), 150)); + break; + } + } + } + + private borderUnCrossed(): void { + this.scroller.setDirection(Direction.None); + if (this.scroller.getIntervalId() != -1) { + clearInterval(this.scroller.getIntervalId()); + this.scroller.setIntervalId(-1); + this.scroller.setScroll(false); + } + } + + private scrollRight(node: DiagramNode, event, cellView) : void { + var sceneWrapper : HTMLDivElement = ( $(".scene-wrapper")[0]); + sceneWrapper.scrollLeft += this.scene.getGridSize() * this.scene.getZoom(); + if (node.getX() + 3 * this.scene.getGridSize() <= DiagramScene.WIDTH) { + this.updateLastCellScrollPosition(event); + node.setPosition( + this.lastCellScrollPosition.x, + this.lastCellScrollPosition.y, + this.scene.getZoom(), + cellView); + } + } + + private scrollLeft(node: DiagramNode, event, cellView) : void { + ( $(".scene-wrapper")[0]).scrollLeft -= this.scene.getGridSize() * this.scene.getZoom(); + if (node.getX() >= this.scene.getGridSize()) { + this.updateLastCellScrollPosition(event); + node.setPosition( + this.lastCellScrollPosition.x, + this.lastCellScrollPosition.y, + this.scene.getZoom(), + cellView); + } + } + + private scrollBottom(node: DiagramNode, event, cellView) : void { + ( $(".scene-wrapper")[0]).scrollTop += this.scene.getGridSize() * this.scene.getZoom(); + if (node.getY() + 3 * this.scene.getGridSize() <= DiagramScene.HEIGHT) { + this.updateLastCellScrollPosition(event); + node.setPosition( + this.lastCellScrollPosition.x, + this.lastCellScrollPosition.y, + this.scene.getZoom(), + cellView); + } + } + + private scrollTop(node: DiagramNode, event, cellView) : void { + ( $(".scene-wrapper")[0]).scrollTop -= this.scene.getGridSize() * this.scene.getZoom(); + if (node.getY() >= this.scene.getGridSize()) { + this.updateLastCellScrollPosition(event); + node.setPosition( + this.lastCellScrollPosition.x, + this.lastCellScrollPosition.y, + this.scene.getZoom(), + cellView); + } + } + + private updateLastCellScrollPosition(event) : void { + var offsetX = (event.pageX - $("#" + this.scene.getId()).offset().left + + $("#" + this.scene.getId()).scrollLeft()) / this.scene.getZoom(); + var offsetY = (event.pageY - $("#" + this.scene.getId()).offset().top + + $("#" + this.scene.getId()).scrollTop()) / this.scene.getZoom(); + var gridSize: number = this.scene.getGridSize(); + offsetX -= offsetX % gridSize; + offsetY -= offsetY % gridSize; + this.lastCellScrollPosition.x = Math.min(offsetX, DiagramScene.WIDTH - 2 * this.scene.getGridSize()); + this.lastCellScrollPosition.y = Math.min(offsetY, DiagramScene.HEIGHT - 2 * this.scene.getGridSize()); + } + private getElementBelow(event: any, checker?: (cell: any) => boolean) { var diagramPaper: HTMLDivElement = document.getElementById(this.scene.getId()); return this.diagramEditorController.getGraph().get('cells').find((cell) => { diff --git a/editor-core/src/main/webapp/app/core/editorCore/model/DiagramScene.ts b/editor-core/src/main/webapp/app/core/editorCore/model/DiagramScene.ts index 31c36ba4..4bcdb4e7 100644 --- a/editor-core/src/main/webapp/app/core/editorCore/model/DiagramScene.ts +++ b/editor-core/src/main/webapp/app/core/editorCore/model/DiagramScene.ts @@ -4,6 +4,9 @@ import {SubprogramNode} from "./SubprogramNode"; import {DiagramElementListener} from "../controller/DiagramElementListener"; export class DiagramScene extends joint.dia.Paper { + public static get WIDTH(): number {return 2000;} + public static get HEIGHT(): number {return 2000;} + private htmlId: string; private graph: joint.dia.Graph; private currentLinkType: string; @@ -20,8 +23,8 @@ export class DiagramScene extends joint.dia.Paper { super({ el: $('#' + htmlId), - width: 2000, - height: 2000, + width: DiagramScene.WIDTH, + height: DiagramScene.HEIGHT, model: graph, gridSize: gridSize, defaultLink: new joint.dia.Link({ diff --git a/editor-core/src/main/webapp/app/core/editorCore/model/Scroller.ts b/editor-core/src/main/webapp/app/core/editorCore/model/Scroller.ts new file mode 100644 index 00000000..f19bc18a --- /dev/null +++ b/editor-core/src/main/webapp/app/core/editorCore/model/Scroller.ts @@ -0,0 +1,42 @@ +export enum Direction { + Up, Down, Left, Right, None +} + +export class Scroller { + + private scroll: boolean; + + private intervalId: number; + + private direction: Direction; + + constructor() { + this.scroll = false; + this.direction = Direction.None; + } + + public getDirection(): Direction { + return this.direction; + } + + public setDirection(value: Direction) { + this.direction = value; + } + + public getIntervalId(): number { + return this.intervalId; + } + + public setIntervalId(value: number) { + this.intervalId = value; + } + + public getScroll(): boolean { + return this.scroll; + } + + public setScroll(value: boolean) { + this.scroll = value; + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index b8015ce8..7c106d84 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,8 @@ 8080 8082 /editor + /editor/robots + /editor/bpmn /auth /dashboard /editorRest @@ -53,6 +55,8 @@ 8080 8080 /editor + /editor/robots + /editor/bpmn /auth /dashboard /editorRest @@ -70,6 +74,8 @@ 8080 8082 /editor + /editor/robots + /editor/bpmn /auth /dashboard /editorRest diff --git a/ui-testing/pom.xml b/ui-testing/pom.xml index 4853448c..8775fe7a 100644 --- a/ui-testing/pom.xml +++ b/ui-testing/pom.xml @@ -5,7 +5,7 @@ 4.0.0 groupId - UI-testing + ui-testing 1.0-SNAPSHOT @@ -52,6 +52,7 @@ exec-maven-plugin org.codehaus.mojo + 1.5.0 Check services @@ -99,17 +100,61 @@ + + + io.github.bonigarcia + webdrivermanager + 1.6.0 + + com.codeborne selenide 4.0 - test - + - io.github.bonigarcia - webdrivermanager - 1.4.10 + org.apache.commons + commons-lang3 + 3.5 + + + + org.jsoup + jsoup + 1.10.2 + + + + + org.springframework + spring-beans + ${springframework.version} + + + org.springframework + spring-context + ${springframework.version} + + + org.springframework + spring-webmvc + ${springframework.version} + + + org.springframework + spring-tx + ${springframework.version} + + + org.springframework.security + spring-security-taglibs + ${springframework.security.version} + + + org.springframework + spring-test + ${springframework.version} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/Page.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/Page.java new file mode 100644 index 00000000..834358bd --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/Page.java @@ -0,0 +1,43 @@ +package com.qreal.wmp.uitesting; + +/** Describes WMP pages in browser. */ +@SuppressWarnings("unchecked") +public enum Page { + + Auth("auth") { + @Override + public T getInstance(PageFactory pageFactory) { + return (T) pageFactory.getAuthPage(); + } + }, + Dashboard("dashboard") { + @Override + public T getInstance(PageFactory pageFactory) { + return (T) pageFactory.getDashboardPage(); + } + }, + EditorRobots("robotsEditor") { + @Override + public T getInstance(PageFactory pageFactory) { + return (T) pageFactory.getEditorPage(); + } + }, + EditorBPMN("bpmnEditor") { + @Override + public T getInstance(PageFactory pageFactory) { + return (T) pageFactory.getEditorPage(); + } + }; + + private String name; + + Page(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public abstract T getInstance(PageFactory pageFactory); +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/PageFactory.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/PageFactory.java new file mode 100644 index 00000000..2c6f89b6 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/PageFactory.java @@ -0,0 +1,56 @@ +package com.qreal.wmp.uitesting; + +import com.qreal.wmp.uitesting.dia.palette.PaletteImpl; +import com.qreal.wmp.uitesting.dia.property.PropertyEditorImpl; +import com.qreal.wmp.uitesting.dia.scene.SceneImpl; +import com.qreal.wmp.uitesting.headerpanel.EditorHeaderPanelImpl; +import com.qreal.wmp.uitesting.pages.AuthPage; +import com.qreal.wmp.uitesting.pages.DashboardPage; +import com.qreal.wmp.uitesting.pages.EditorPage; +import com.qreal.wmp.uitesting.pages.EventProvider; +import org.openqa.selenium.WebDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.codeborne.selenide.Selenide.title; + +/** Returns page instance for requested uri. */ +public class PageFactory { + + private static final Logger logger = LoggerFactory.getLogger(PageFactory.class); + + private final WebDriver webDriver; + + private final EventProvider eventProvider; + + public PageFactory(WebDriver webDriver) { + this.webDriver = webDriver; + eventProvider = new EventProvider(); + } + + /** Returns Editor Page instance. */ + public EditorPage getEditorPage() { + logger.info("Editor page was created"); + EditorPage page = new EditorPage( + title(), + SceneImpl.getScene(webDriver), + PaletteImpl.getPalette(), + PropertyEditorImpl.getPropertyEditor(), + EditorHeaderPanelImpl.getEditorHeaderPanel(this, webDriver, eventProvider) + ); + eventProvider.addListener(page); + return page; + } + + /** Returns Dashboard Page instance. */ + public DashboardPage getDashboardPage() { + logger.info("Dashboard page was created"); + return new DashboardPage(title()); + } + + /** Returns Auth Page instance. */ + public AuthPage getAuthPage() { + logger.info("Auth page was created"); + return new AuthPage(title()); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/PageLoader.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/PageLoader.java new file mode 100644 index 00000000..eb94332e --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/PageLoader.java @@ -0,0 +1,42 @@ +package com.qreal.wmp.uitesting; + +import com.qreal.wmp.uitesting.exceptions.WrongAuthException; +import com.qreal.wmp.uitesting.services.Auther; +import com.qreal.wmp.uitesting.services.Opener; + +/** + * Loads page. + * It means, firstly, it opens uri by Opener service. + * Secondly, it returns page by PageFactory. + */ +public class PageLoader { + + private final PageFactory pageFactory; + + private final Opener opener; + + private final Auther auther; + + public PageLoader(PageFactory pageFactory, Opener opener, Auther auther) { + this.pageFactory = pageFactory; + this.opener = opener; + this.auther = auther; + } + + /** Loads and returns requested page with default authentication. */ + public T load(Page page) { + opener.open(page.getName()); + return getPage(page); + } + + /** Loads and returns requested page with login and password. */ + public T load(Page page, String username, String password) throws WrongAuthException { + auther.auth(username, password); + opener.open(page.getName()); + return getPage(page); + } + + private T getPage(Page page) { + return page.getInstance(pageFactory); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/config/AppInit.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/config/AppInit.java new file mode 100644 index 00000000..179b4bda --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/config/AppInit.java @@ -0,0 +1,18 @@ +package com.qreal.wmp.uitesting.config; + +import io.github.bonigarcia.wdm.ChromeDriverManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan("com.qreal.wmp.uitesting") +public class AppInit { + + /** Main function creates context. */ + public static void main(final String... args) { + ChromeDriverManager.getInstance().setup(); + final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan("com.qreal.wmp.uitesting"); + context.register(AppInit.class); + context.refresh(); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/config/DevConfig.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/config/DevConfig.java new file mode 100644 index 00000000..5632aa9a --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/config/DevConfig.java @@ -0,0 +1,67 @@ +package com.qreal.wmp.uitesting.config; + +import com.codeborne.selenide.WebDriverRunner; +import com.qreal.wmp.uitesting.PageFactory; +import com.qreal.wmp.uitesting.PageLoader; +import com.qreal.wmp.uitesting.services.Auther; +import com.qreal.wmp.uitesting.services.Opener; +import com.qreal.wmp.uitesting.services.impl.AutherImpl; +import com.qreal.wmp.uitesting.services.impl.OpenerImpl; +import io.github.bonigarcia.wdm.ChromeDriverManager; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.env.Environment; + +import java.util.concurrent.TimeUnit; + +/** Creates beans for Spring needs. **/ +@Configuration +@PropertySource("classpath:pages.properties") +public class DevConfig { + + @Autowired + private Environment environment; + + /** Processor for Environment linked to property files.*/ + @Bean + public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public WebDriver webDriver() { + ChromeDriverManager.getInstance().setup(); + WebDriver driver = new ChromeDriver(); + driver.manage().window().maximize(); + driver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS); + driver.manage().timeouts().setScriptTimeout(3, TimeUnit.SECONDS); + driver.manage().timeouts().pageLoadTimeout(20, TimeUnit.SECONDS); + WebDriverRunner.setWebDriver(driver); + return driver; + } + + @Bean + public Auther auther() { + return new AutherImpl(environment); + } + + @Bean + public Opener opener() { + return new OpenerImpl(environment, auther()); + } + + @Bean + public PageFactory pageFactory() { + return new PageFactory(webDriver()); + } + + @Bean + public PageLoader pageLoader() { + return new PageLoader(pageFactory(), opener(), auther()); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/Palette.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/Palette.java new file mode 100644 index 00000000..111c0f43 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/Palette.java @@ -0,0 +1,18 @@ +package com.qreal.wmp.uitesting.dia.palette; + +import org.openqa.selenium.NoSuchElementException; + +/** + * Describes Palette. + * For any manipulating with it. + */ +public interface Palette { + + /** + * Chooses an element from Palette. + * + * @param elementName name of block + * @return block + */ + PaletteElement getElement(String elementName) throws NoSuchElementException; +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/PaletteElement.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/PaletteElement.java new file mode 100644 index 00000000..6ae9e640 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/PaletteElement.java @@ -0,0 +1,25 @@ +package com.qreal.wmp.uitesting.dia.palette; + +import com.codeborne.selenide.SelenideElement; + + +/** Describes palette's items. */ +public class PaletteElement { + + private final SelenideElement innerSeleniumELement; + + private final String name; + + public PaletteElement(SelenideElement innerSeleniumELement) { + this.innerSeleniumELement = innerSeleniumELement; + name = innerSeleniumELement.attr("data-type"); + } + + public SelenideElement getInnerSeleniumELement() { + return innerSeleniumELement; + } + + public String getName() { + return name; + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/PaletteImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/PaletteImpl.java new file mode 100644 index 00000000..8f3d1ef3 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/palette/PaletteImpl.java @@ -0,0 +1,28 @@ +package com.qreal.wmp.uitesting.dia.palette; + +import com.codeborne.selenide.SelenideElement; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.codeborne.selenide.Selectors.withText; +import static com.codeborne.selenide.Selenide.$; + +/** {@inheritDoc} */ +public class PaletteImpl implements Palette { + + private static final String SELECTOR = "#palette-tab-content"; + + private static final Logger logger = LoggerFactory.getLogger(PaletteImpl.class); + + public PaletteElement getElement(final String elementName) throws NoSuchElementException { + final SelenideElement element = $(By.cssSelector(SELECTOR)).find(withText(elementName)); + logger.info("Get element {} from Palette", element); + return new PaletteElement(element); + } + + public static Palette getPalette() { + return new PaletteImpl(); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/property/PropertyEditor.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/property/PropertyEditor.java new file mode 100644 index 00000000..1d96ef40 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/property/PropertyEditor.java @@ -0,0 +1,16 @@ +package com.qreal.wmp.uitesting.dia.property; + +import com.codeborne.selenide.SelenideElement; +import org.openqa.selenium.NoSuchElementException; + +/** + * Describe Property Editor. + * When block is clicked, it could be configured by Property Editor. + */ +public interface PropertyEditor { + /** Set property of element which on the focus. */ + void setProperty(SelenideElement element, String propertyName, String propertyValue) throws NoSuchElementException; + + /** Return the value of property by name. */ + String getProperty(final SelenideElement element, final String propertyName) throws NoSuchElementException; +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/property/PropertyEditorImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/property/PropertyEditorImpl.java new file mode 100644 index 00000000..1200616e --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/property/PropertyEditorImpl.java @@ -0,0 +1,67 @@ +package com.qreal.wmp.uitesting.dia.property; + +import com.codeborne.selenide.SelenideElement; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.OptionalInt; +import java.util.stream.IntStream; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$; + +/** {@inheritDoc} */ +public class PropertyEditorImpl implements PropertyEditor { + + private static final String SELECTOR = "#property_table"; + + private static final Logger logger = LoggerFactory.getLogger(PropertyEditorImpl.class); + + /** {@inheritDoc} */ + public void setProperty(final SelenideElement element, final String propertyName, final String propertyValue) + throws NoSuchElementException + { + element.click(); + SelenideElement property = getInputOfElement(propertyName); + if (property.attr("class").equals("input-group")) { + property.find(By.xpath(".//*")).setValue(propertyValue); + } else { + property.selectOptionByValue(propertyValue); + } + logger.info("Set property {} to {}", propertyName, propertyValue); + } + + /** {@inheritDoc} */ + public String getProperty(final SelenideElement element, final String propertyName) throws NoSuchElementException { + $(By.cssSelector(SELECTOR)).click(); + element.click(); + SelenideElement property = getInputOfElement(propertyName); + logger.info("Get value of preperty {}", propertyName); + if (property.attr("class").equals("input-group")) { + return property.find(By.xpath(".//*")).getValue(); + } else { + return property.getSelectedOption().getValue(); + } + } + + public static PropertyEditor getPropertyEditor() { + return new PropertyEditorImpl(); + } + + /** To set/get property we need to take web element which describes needed field. */ + private SelenideElement getInputOfElement(final String propertyName) { + final List allChilds = $$(By.cssSelector(SELECTOR + " tbody > * > *")); + final OptionalInt indexOfNeeded = IntStream.range(0, allChilds.size()) + .filter(index -> allChilds.get(index).getText().contains(propertyName)) + .findFirst(); + if (!indexOfNeeded.isPresent()) { + throw new NoSuchElementException("There is no property with name " + propertyName); + } + return $(By.cssSelector(SELECTOR + " tbody > *:nth-of-type(" + + (indexOfNeeded.getAsInt() / 2 + 1) + + ") > *:nth-of-type(2) > *" )); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/Coordinate.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/Coordinate.java new file mode 100644 index 00000000..d6d08559 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/Coordinate.java @@ -0,0 +1,64 @@ +package com.qreal.wmp.uitesting.dia.scene; + +import com.codeborne.selenide.SelenideElement; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * Describes element's position on the Scene. + * Contains absolute coordinates of scene, which are written in 'transform' tag on the html representation. + * Also contains cell's position (the Scene is represented by a mesh of cells) + */ +public class Coordinate { + + public static final String SELECTOR = "transform"; + + public static final int POINT_IN_CELL = 25; + + private final int xAbsolute; + + private final int yAbsolute; + + /** Returns coordinate of object on scene. */ + @NotNull + public static Optional getCoordinateFromSeleniumObject(SelenideElement element) { + final String position = element.attr(SELECTOR); + final String[] pairStr = position.substring(position.indexOf('(') + 1, position.indexOf(')')).split(","); + return Optional.of( + new Coordinate( + Double.valueOf(pairStr[0]).intValue(), + Double.valueOf(pairStr[1]).intValue() + ) + ); + } + + public Coordinate(int xAbsolute, int yAbsolute) { + this.xAbsolute = xAbsolute; + this.yAbsolute = yAbsolute; + } + + public int getXCell() { + return xAbsolute / POINT_IN_CELL; + } + + public int getYCell() { + return yAbsolute / POINT_IN_CELL; + } + + public int getXAbsolute() { + return xAbsolute; + } + + public int getYAbsolute() { + return yAbsolute; + } + + public boolean equals(final Coordinate other) { + return xAbsolute == other.getXAbsolute() && yAbsolute == other.getYAbsolute(); + } + + public String toString() { + return "(" + getXAbsolute() + "," + getYAbsolute() + ")"; + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/Scene.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/Scene.java new file mode 100644 index 00000000..778dfe85 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/Scene.java @@ -0,0 +1,35 @@ +package com.qreal.wmp.uitesting.dia.scene; + +import com.qreal.wmp.uitesting.dia.palette.PaletteElement; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.Link; +import com.qreal.wmp.uitesting.dia.scene.elements.SceneElement; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; + +import java.util.List; + +public interface Scene { + /** Drag element from scene or palette and put it on the center of scene. */ + Block dragAndDrop(PaletteElement paletteElement); + + /** Drag element from scene or palette and put it in cell of the scene. */ + Block dragAndDrop(PaletteElement element, int cellX, int cellY); + + /** Move element from scene to the cell. */ + void moveToCell(Block block, int cellX, int cellY); + + /** Check if element exist on the scene. */ + boolean exist(SceneElement element); + + /** Remove block from the scene. */ + void remove(SceneElement element) throws ElementNotOnTheSceneException; + + /** Add link between two elements. */ + Link addLink(Block source, Block target); + + /** Return all blocks. */ + List getBlocks(); + + /** Remove all elements from the scene. */ + void clean(); +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/SceneImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/SceneImpl.java new file mode 100644 index 00000000..28480459 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/SceneImpl.java @@ -0,0 +1,173 @@ +package com.qreal.wmp.uitesting.dia.scene; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.google.common.base.Predicate; +import com.qreal.wmp.uitesting.dia.palette.PaletteElement; +import com.qreal.wmp.uitesting.dia.palette.PaletteImpl; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.Link; +import com.qreal.wmp.uitesting.dia.scene.elements.SceneElement; +import com.qreal.wmp.uitesting.dia.scene.providers.BlockProvider; +import com.qreal.wmp.uitesting.dia.scene.providers.LinkProvider; +import com.qreal.wmp.uitesting.dia.scene.window.SceneWindow; +import com.qreal.wmp.uitesting.dia.scene.window.SceneWindowImpl; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; +import com.qreal.wmp.uitesting.pages.Resettable; +import org.jetbrains.annotations.Contract; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static com.codeborne.selenide.Selenide.$; + +/** + * Describes Scene of Editor. + * Can add, rm and manipulate with objects on that area. + */ +public class SceneImpl implements Scene, Resettable { + + public static final String SELECTOR = ".scene-wrapper"; + + private static final Logger logger = LoggerFactory.getLogger(PaletteImpl.class); + + private final WebDriver webDriver; + + private final SceneWindow sceneWindow; + + private final BlockProvider blockProvider; + + private final LinkProvider linkProvider; + + /** For actions such as mouse move we need driver of current page. */ + private SceneImpl(WebDriver webDriver) { + this.webDriver = webDriver; + // For actions such as mouse move we need driver of current page. + if (webDriver instanceof JavascriptExecutor) { + ((JavascriptExecutor) webDriver).executeScript( + createDiv("SceneWindowLeft") + createDiv("SceneWindowTop") + + createDiv("SceneWindowHorSize") + createDiv("SceneWindowVerSize") + ); + } + sceneWindow = SceneWindowImpl.getSceneWindow(webDriver); + blockProvider = BlockProvider.getBlockProvider(sceneWindow, SELECTOR, this); + linkProvider = LinkProvider.getLinkProvider(SELECTOR, webDriver); + } + + @Override + public Block dragAndDrop(final PaletteElement element) { + element.getInnerSeleniumELement().dragAndDropTo(SELECTOR); + return blockProvider.getNewBlock(); + } + + @Override + public Block dragAndDrop(final PaletteElement element, int cellX, int cellY) { + Block newBlock = dragAndDrop(element); + blockProvider.moveToCell(newBlock, cellX, cellY); + return newBlock; + } + + @Override + public void moveToCell(Block block, int cellX, int cellY) { + blockProvider.moveToCell(block, cellX, cellY); + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean exist(SceneElement element) { + if (element instanceof Block) { + blockProvider.recalculateBlocks(); + return blockProvider.exist((Block) element); + } + if (element instanceof Link) { + linkProvider.recalculateLinks(); + return linkProvider.exist((Link) element); + } + return false; + } + + @Override + public void remove(SceneElement element) throws ElementNotOnTheSceneException { + if (element instanceof Link) { + removeSceneElement(((Link) element).getTarget()); + } else { + removeSceneElement(element); + } + } + + @Override + public Link addLink(final Block source, final Block target) { + return linkProvider.addLink(source, target); + } + + @Override + public List getBlocks() { + return blockProvider.getBlocks(); + } + + @Override + public void clean() { + blockProvider.recalculateBlocks(); + linkProvider.recalculateLinks(); + if (!linkProvider.isEmpty()) { + try { + remove(linkProvider.getLinks().get(0)); + } catch (ElementNotOnTheSceneException e) { + logger.error("It's impossible to remove link, because it is not on the Scene."); + } + clean(); + } else { + if (!blockProvider.isEmpty()) { + try { + remove(blockProvider.getBlocks().get(0)); + } catch (ElementNotOnTheSceneException e) { + logger.error("It's impossible to remove block, because it is not on the scene."); + } + clean(); + } else { + logger.info("Clean scene"); + } + } + } + + @Override + public void reset() { + blockProvider.recalculateBlocks(); + linkProvider.recalculateLinks(); + } + + @Contract("_ -> !null") + public static Scene getScene(WebDriver webDriver) { + return new SceneImpl(webDriver); + } + + @Contract(pure = true) + private static String createDiv(String divName) { + return "$('body').append('');"; + } + + @Contract("null -> fail") + private void removeSceneElement(SceneElement sceneElement) throws ElementNotOnTheSceneException { + sceneWindow.focus(sceneElement.getCoordinateOnScene()); + logger.info("Remove element {} form scene", sceneElement.getInnerSeleniumElement().toString()); + new Actions(webDriver) + .contextClick(sceneElement.getInnerSeleniumElement()) + .build() + .perform(); + SelenideElement contextMenu = $(By.id("scene-context-menu")); + contextMenu.shouldBe(Condition.visible); + contextMenu.click(); + (new WebDriverWait(webDriver, 5)) + .until((Predicate) webDriver -> { + assert webDriver != null; + return webDriver.findElements(sceneElement.getBy()).size() == 0; + }); + + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/Block.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/Block.java new file mode 100644 index 00000000..0b593b86 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/Block.java @@ -0,0 +1,42 @@ +package com.qreal.wmp.uitesting.dia.scene.elements; + +import com.qreal.wmp.uitesting.dia.scene.Scene; +import org.openqa.selenium.By; + +import static com.codeborne.selenide.Selenide.$; + +/** + * Describes item, which is placed on the scene. + * Palette have items. If we dragAndDrop these items to the Scene, we'll get Blocks. + */ +public class Block extends SceneElementImpl { + + public static final String CLASS_NAME = "element devs ImageWithPorts"; + + private static final String PORT_CLASS_NAME = "port0"; + + private final String name; + + private final SceneElement port; + + private final Scene scene; + + public Block(String name, By selector, Scene scene) { + super(selector); + this.name = name; + this.port = new SceneElementImpl(By.id($(selector).find(By.className(PORT_CLASS_NAME)).attr("id"))); + this.scene = scene; + } + + public String getName() { + return name; + } + + public SceneElement getPort() { + return port; + } + + public void moveToCell(int cellX, int cellY) { + scene.moveToCell(this, cellX, cellY); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/Link.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/Link.java new file mode 100644 index 00000000..6872a66d --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/Link.java @@ -0,0 +1,52 @@ +package com.qreal.wmp.uitesting.dia.scene.elements; + +import com.codeborne.selenide.SelenideElement; +import com.qreal.wmp.uitesting.dia.scene.Coordinate; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; +import org.openqa.selenium.By; + +import static com.codeborne.selenide.Selenide.$; + +/** Link describes relations between blocks. */ +public class Link extends SceneElementImpl { + + public static final String CLASS_NAME = "link"; + + private static final String ARROWHEAD = "marker-arrowheads"; + + private final String name; + + private final SceneElement source; + + private final SceneElement target; + + /** Describes link between two blocks. */ + public Link(String name, By selector) { + super(selector); + this.name = name; + SelenideElement source = $(selector).find(By.className(ARROWHEAD)).find(By.cssSelector(":nth-child(1)")); + this.source = new SceneElementImpl(By.id(source.attr("id"))); + SelenideElement target = $(selector).find(By.className(ARROWHEAD)).find(By.cssSelector(":nth-child(2)")); + this.target = new SceneElementImpl(By.id(target.attr("id"))); + } + + public String getName() { + return name; + } + + public SceneElement getSource() { + return source; + } + + public SceneElement getTarget() { + return target; + } + + @Override + public Coordinate getCoordinateOnScene() throws ElementNotOnTheSceneException { + return new Coordinate( + (source.getCoordinateOnScene().getXAbsolute() + target.getCoordinateOnScene().getXAbsolute()) / 2, + (source.getCoordinateOnScene().getYAbsolute() + target.getCoordinateOnScene().getYAbsolute()) / 2 + ); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/SceneElement.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/SceneElement.java new file mode 100644 index 00000000..cba5a1cf --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/SceneElement.java @@ -0,0 +1,15 @@ +package com.qreal.wmp.uitesting.dia.scene.elements; + +import com.codeborne.selenide.SelenideElement; +import com.qreal.wmp.uitesting.dia.scene.Coordinate; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; +import org.openqa.selenium.By; + +/** Describes any element on the Scene. */ +public interface SceneElement { + SelenideElement getInnerSeleniumElement(); + + By getBy(); + + Coordinate getCoordinateOnScene() throws ElementNotOnTheSceneException; +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/SceneElementImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/SceneElementImpl.java new file mode 100644 index 00000000..7d0d99d4 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/elements/SceneElementImpl.java @@ -0,0 +1,36 @@ +package com.qreal.wmp.uitesting.dia.scene.elements; + +import com.codeborne.selenide.SelenideElement; +import com.qreal.wmp.uitesting.dia.scene.Coordinate; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; +import org.openqa.selenium.By; + +import static com.codeborne.selenide.Selenide.$; + +/** + * All Scene elements have selector by which we can clearly define their web instances. + * Also all scene elements have coordinates on the Scene. + */ +public class SceneElementImpl implements SceneElement { + + // Wrapper over an string selector. Used to search the element in HTML representation of current page. + private final By selector; + + public SceneElementImpl(By selector) { + this.selector = selector; + } + + /** Based on the Selenium element. */ + public SelenideElement getInnerSeleniumElement() { + return $(selector); + } + + public By getBy() { + return selector; + } + + public Coordinate getCoordinateOnScene() throws ElementNotOnTheSceneException { + return Coordinate.getCoordinateFromSeleniumObject(getInnerSeleniumElement()) + .orElseThrow(ElementNotOnTheSceneException::new); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/providers/BlockProvider.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/providers/BlockProvider.java new file mode 100644 index 00000000..c098da99 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/providers/BlockProvider.java @@ -0,0 +1,93 @@ +package com.qreal.wmp.uitesting.dia.scene.providers; + +import com.codeborne.selenide.SelenideElement; +import com.qreal.wmp.uitesting.dia.scene.Coordinate; +import com.qreal.wmp.uitesting.dia.scene.Scene; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.window.SceneWindow; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; +import org.jetbrains.annotations.Contract; +import org.openqa.selenium.By; +import org.openqa.selenium.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.codeborne.selenide.Selenide.$$; + +/** Encapsulates blocks operations. */ +public class BlockProvider { + + private static final Logger logger = LoggerFactory.getLogger(BlockProvider.class); + + private final SceneWindow sceneWindow; + + private final String selector; + + private final Scene scene; + + private Set blocks = new HashSet<>(); + + private BlockProvider(SceneWindow sceneWindow, String selector, Scene scene) { + this.sceneWindow = sceneWindow; + this.selector = selector; + this.scene = scene; + } + + /** Move element to cell with x and y coordinates. */ + public void moveToCell(final Block block, final int cellX, final int cellY) { + logger.info("Move element {} to cell ({}, {})", block, cellX, cellY); + try { + sceneWindow.move(block, + new Coordinate(cellX * Coordinate.POINT_IN_CELL, cellY * Coordinate.POINT_IN_CELL)); + } catch (ElementNotOnTheSceneException e) { + logger.error("It is impossible to move element, which is not on the Scene"); + } + } + + public List getBlocks() { + return Collections.unmodifiableList(blocks.stream().collect(Collectors.toList())); + } + + /** Return added block. */ + public Block getNewBlock() { + final SelenideElement newEl = updateBlocks().orElseThrow(NotFoundException::new); + logger.info("Add element {} to scene", newEl); + Block block = new Block(newEl.attr("id"), By.id(newEl.attr("id")), scene); + blocks.add(block); + return block; + } + + public boolean exist(Block block) { + return blocks.stream().anyMatch(anyBlock -> anyBlock.getName().equals(block.getName())); + } + + public boolean isEmpty() { + return blocks.isEmpty(); + } + + public void recalculateBlocks() { + blocks = $$(By.cssSelector(selector + " #v_7 > *")).stream() + .filter(x -> x.attr("class").contains(Block.CLASS_NAME)) + .map(x -> new Block(x.attr("id"), By.id(x.attr("id")), scene)) + .collect(Collectors.toSet()); + } + + @Contract("_, _, _ -> !null") + public static BlockProvider getBlockProvider(SceneWindow sceneWindow, String selector, Scene scene) { + return new BlockProvider(sceneWindow, selector, scene); + } + + /** Return new element of the scene. */ + private Optional updateBlocks() { + final List allElements = $$(By.cssSelector(selector + " #v_7 > *")); + return allElements.stream() + .filter(htmlElement -> + htmlElement.attr("class").contains("element devs ImageWithPorts") && + blocks.stream().noneMatch(block -> block.getInnerSeleniumElement() + .attr("id").equals(htmlElement.attr("id"))) + ).findFirst(); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/providers/LinkProvider.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/providers/LinkProvider.java new file mode 100644 index 00000000..8e193643 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/providers/LinkProvider.java @@ -0,0 +1,84 @@ +package com.qreal.wmp.uitesting.dia.scene.providers; + +import com.codeborne.selenide.SelenideElement; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.Link; +import org.jetbrains.annotations.Contract; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.interactions.Actions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$; + +/** Encapsulates links operations. */ +public class LinkProvider { + + private static final Logger logger = LoggerFactory.getLogger(LinkProvider.class); + + private final String selector; + + private final WebDriver webDriver; + + private Set links = new HashSet<>(); + + private LinkProvider(String selector, WebDriver webDriver) { + this.selector = selector; + this.webDriver = webDriver; + } + + public List getLinks() { + return Collections.unmodifiableList(links.stream().collect(Collectors.toList())); + } + + public boolean isEmpty() { + return links.isEmpty(); + } + + public boolean exist(Link link) { + return links.stream().anyMatch(anyLink -> anyLink.getName().equals(link.getName())); + } + + /** Add link between two blocks. */ + public Link addLink(final Block source, final Block target) { + final SelenideElement begin = $(By.cssSelector(selector + " #" + + source.getInnerSeleniumElement().attr("id") + " .outPorts")); + logger.info("Begin element {}, end element {} ", begin, target); + new Actions(webDriver) + .release() + .dragAndDrop(source.getPort().getInnerSeleniumElement(), target.getInnerSeleniumElement()) + .build().perform(); + SelenideElement newEl = updateLinks().orElseThrow(() -> new NoSuchElementException("Link was not created")); + logger.info("Add link {}", newEl); + Link res = new Link(newEl.attr("id"), By.id(newEl.attr("id"))); + links.add(res); + return res; + } + + public void recalculateLinks() { + links = $$(By.cssSelector(selector + " #v_7 > *")).stream() + .filter(x -> x.attr("class").contains(Link.CLASS_NAME)) + .map(x -> new Link(x.attr("id"), By.id(x.attr("id")))) + .collect(Collectors.toSet()); + } + + @Contract("_, _ -> !null") + public static LinkProvider getLinkProvider(String selector, WebDriver webDriver) { + return new LinkProvider(selector, webDriver); + } + + private Optional updateLinks() { + final List allElements = $$(By.cssSelector(selector + " #v_7 > *")); + return allElements.stream() + .filter(htmlElement -> + htmlElement.attr("class").contains("link") && + links.stream().noneMatch(link -> htmlElement.attr("id") + .equals(link.getInnerSeleniumElement().attr("id"))) + ).findFirst(); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/window/SceneWindow.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/window/SceneWindow.java new file mode 100644 index 00000000..ebe5d8e9 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/window/SceneWindow.java @@ -0,0 +1,23 @@ +package com.qreal.wmp.uitesting.dia.scene.window; + +import com.qreal.wmp.uitesting.dia.scene.Coordinate; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; + +public interface SceneWindow { + + /** + * Moves element to the requested position. + * + * @param element element to move + * @param dist position to move + * */ + void move(final Block element, final Coordinate dist) throws ElementNotOnTheSceneException; + + /** + * Move the screen to requested position. + * + * @param coordinate coordinate to move + */ + void focus(final Coordinate coordinate); +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/window/SceneWindowImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/window/SceneWindowImpl.java new file mode 100644 index 00000000..adee0086 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/dia/scene/window/SceneWindowImpl.java @@ -0,0 +1,187 @@ +package com.qreal.wmp.uitesting.dia.scene.window; + +import com.google.common.base.Predicate; +import com.qreal.wmp.uitesting.dia.scene.Coordinate; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.SceneElement; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; +import org.jetbrains.annotations.Contract; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Random; + +import static com.codeborne.selenide.Selenide.$; + +/** + * Describes part of the scene, which is shown on browser. + */ +public class SceneWindowImpl implements SceneWindow { + + private static final Logger logger = LoggerFactory.getLogger(SceneWindowImpl.class); + + private final WebDriver driver; + + /** Constructor takes links to current scene and current driver. */ + private SceneWindowImpl(final WebDriver driver) { + this.driver = driver; + } + + /** + * Moves element to the requested position. + * + * @param element element to move + * @param dist position to move + */ + @Override + public void move(final Block element, final Coordinate dist) throws ElementNotOnTheSceneException { + Coordinate src = element.getCoordinateOnScene(); + focus(src); + int sizeHor = Double.valueOf($(By.id("SceneWindowHorSize")).innerHtml()).intValue(); + int sizeVer = Double.valueOf($(By.id("SceneWindowVerSize")).innerHtml()).intValue(); + + Actions actions = new Actions(driver); + actions.clickAndHold(element.getInnerSeleniumElement()); + if (isDistanceLessThenBarrier( + dist, + element.getCoordinateOnScene(), + new OffsetObject(sizeHor / 2, sizeVer / 2))) { + + jump(actions, element, dist); + } else { + java.util.function.Predicate condX = x -> + isDistanceLessThenBarrier(x.getXAbsolute(), dist.getXAbsolute(), sizeHor / 3); + + if (src.getXAbsolute() < dist.getXAbsolute()) { + innerMove(actions, element, new OffsetObject(sizeHor / 4, 0), condX); + } else { + innerMove(actions, element, new OffsetObject(-sizeHor / 4, 0), condX); + } + } + if (Math.abs(dist.getXAbsolute() - element.getCoordinateOnScene().getXAbsolute()) < sizeHor / 2 + && Math.abs(dist.getYAbsolute() - element.getCoordinateOnScene().getYAbsolute()) < sizeVer / 2) { + jump(actions, element, dist); + } else { + java.util.function.Predicate condY = x -> + isDistanceLessThenBarrier(x.getYAbsolute(), dist.getYAbsolute(), sizeVer / 3); + if (src.getYAbsolute() < dist.getYAbsolute()) { + innerMove(actions, element, new OffsetObject(0, sizeVer / 4), condY); + } else { + innerMove(actions, element, new OffsetObject(0, -sizeVer / 4), condY); + } + } + + jump(actions, element, dist); + + (new WebDriverWait(driver, 40)) + .until((Predicate) webDriver -> { + try { + return element.getCoordinateOnScene().equals(dist); + } catch (ElementNotOnTheSceneException e) { + logger.error("It is impossible to move element, which is not on the Scene"); + } + return false; + }); + } + + @Override + public void focus(final Coordinate coordinate) { + updateCanvasInfo(); + int sizeHor = Double.valueOf($(By.id("SceneWindowHorSize")).innerHtml()).intValue(); + int sizeVer = Double.valueOf($(By.id("SceneWindowVerSize")).innerHtml()).intValue(); + + logger.info("Focus to " + coordinate.getXAbsolute() + " " + coordinate.getYAbsolute()); + if (driver instanceof JavascriptExecutor) { + ((JavascriptExecutor) driver).executeScript("var canvas = " + + "document.getElementsByClassName(\"scene-wrapper\")[0]; " + + "var BB=canvas.getBoundingClientRect();" + + "canvas.scrollLeft = " + Math.max(0, (coordinate.getXAbsolute() - sizeHor / 2)) + "; " + + "canvas.scrollTop = " + Math.max(0, (coordinate.getYAbsolute() - sizeVer / 2)) + ";" + ); + } + updateCanvasInfo(); + } + + @Contract("_ -> !null") + public static SceneWindow getSceneWindow(WebDriver webDriver) { + return new SceneWindowImpl(webDriver); + } + + private void updateCanvasInfo() { + if (driver instanceof JavascriptExecutor) { + ((JavascriptExecutor) driver).executeScript("var canvas = " + + "document.getElementsByClassName(\"scene-wrapper\")[0]; " + + "var BB=canvas.getBoundingClientRect();" + + "$('#SceneWindowLeft').html(canvas.scrollLeft);" + + "$('#SceneWindowTop').html(canvas.scrollTop);" + + "$('#SceneWindowHorSize').html(BB.right - BB.left);" + + "$('#SceneWindowVerSize').html(BB.bottom - BB.top);" + ); + } + } + + private void innerMove(Actions actions, SceneElement element, + OffsetObject offset, java.util.function.Predicate cond) { + + (new WebDriverWait(driver, 40)) + .until((Predicate) webDriver -> { + try { + Coordinate current = element.getCoordinateOnScene(); + actions.moveToElement(element.getInnerSeleniumElement()).perform(); + actions.moveByOffset(offset.offsetX, offset.offsetY).perform(); + + focus(element.getCoordinateOnScene()); + if (element.getCoordinateOnScene().equals(current)) { + Random random = new Random(); + int signX = (int) Math.signum(offset.offsetX); + int signY = (int) Math.signum(offset.offsetY); + if (offset.offsetX != 0) { + offset.offsetX = signX * random.nextInt(Math.abs(offset.offsetX)); + } + if (offset.offsetY != 0) { + offset.offsetY = signY * random.nextInt(Math.abs(offset.offsetY)); + } + } + return cond.test(element.getCoordinateOnScene()); + } catch (ElementNotOnTheSceneException e) { + logger.error("It is impossible to move element, which is not on the Scene"); + } + return false; + }); + } + + private void jump(Actions actions, SceneElement element, Coordinate dist) throws ElementNotOnTheSceneException { + if (!element.getCoordinateOnScene().equals(dist)) { + focus(element.getCoordinateOnScene()); + actions.moveToElement(element.getInnerSeleniumElement()).moveByOffset( + dist.getXAbsolute() - element.getCoordinateOnScene().getXAbsolute(), + dist.getYAbsolute() - element.getCoordinateOnScene().getYAbsolute() + ).release().build().perform(); + } + } + + private class OffsetObject { + private int offsetX; + + private int offsetY; + + OffsetObject(int offsetX, int offsetY) { + this.offsetX = offsetX; + this.offsetY = offsetY; + } + } + + private boolean isDistanceLessThenBarrier(double fst, double snd, double barrier) { + return Math.abs(fst - snd) <= barrier; + } + + private boolean isDistanceLessThenBarrier(Coordinate fst, Coordinate snd, OffsetObject barrier) { + return isDistanceLessThenBarrier(fst.getXAbsolute(), snd.getXAbsolute(), barrier.offsetX) && + isDistanceLessThenBarrier(fst.getYAbsolute(), snd.getYAbsolute(), barrier.offsetY); + } +} \ No newline at end of file diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/exceptions/ElementNotOnTheSceneException.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/exceptions/ElementNotOnTheSceneException.java new file mode 100644 index 00000000..53197584 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/exceptions/ElementNotOnTheSceneException.java @@ -0,0 +1,9 @@ +package com.qreal.wmp.uitesting.exceptions; + +public class ElementNotOnTheSceneException extends Exception { + + public ElementNotOnTheSceneException() { + super("It is impossible to get Coordinates of element, which is not on the Scene"); + } + +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/exceptions/WrongAuthException.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/exceptions/WrongAuthException.java new file mode 100644 index 00000000..ec2aa1d1 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/exceptions/WrongAuthException.java @@ -0,0 +1,9 @@ +package com.qreal.wmp.uitesting.exceptions; + +/** Throw if we cannot authorize. */ +public class WrongAuthException extends Exception { + + public WrongAuthException(final String login, final String password) { + super("Unable to authorize with login: " + login + " and password: " + password); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/DiagramStoreService.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/DiagramStoreService.java new file mode 100644 index 00000000..82a03e4e --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/DiagramStoreService.java @@ -0,0 +1,120 @@ +package com.qreal.wmp.uitesting.headerpanel; + +import com.codeborne.selenide.SelenideElement; +import com.qreal.wmp.uitesting.dia.scene.SceneImpl; +import com.qreal.wmp.uitesting.headerpanel.folderwindow.FolderAreaImpl; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.openqa.selenium.By; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static com.codeborne.selenide.Condition.text; +import static com.codeborne.selenide.Selenide.$; + +/** + * Keeps custom html representations of saved diagrams. + * Also makes final steps of save/open action in folder menu. + */ +public class DiagramStoreService { + + private final Map diagrams = new HashMap<>(); + + private final By sceneSelector = By.cssSelector(SceneImpl.SELECTOR); + + private String lastKnownKey; + + /** Saves diagram. */ + public void saveDiagram(String key) { + SelenideElement element = $(By.cssSelector(".saving-menu")).find(By.cssSelector(":nth-child(2)")); + element.setValue(getFilename(key)); + diagrams.put(key, prepareElement(Jsoup.parseBodyFragment($(sceneSelector).innerHtml()).body())); + $(By.id("saving")).click(); + lastKnownKey = key; + } + + /** + * Key is the last knownKey. + * Call when user click save button (not SaveAs). + */ + public void saveDiagram() { + diagrams.put(lastKnownKey, prepareElement(Jsoup.parseBodyFragment($(sceneSelector).innerHtml()).body())); + } + + /** Check if current diagram(now in the scene) equals diagram which is kept in store. */ + public boolean equalsDrigrams(String key) { + return diagrams.get(key).toString().equals(prepareElement( + Jsoup.parseBodyFragment($(sceneSelector).innerHtml()).body()).toString() + ); + } + + /** Opens diagram. */ + public void openDiagram(String key) { + String filename = getFilename(key); + $(FolderAreaImpl.selector) + .findAll(By.className("diagrams")) + .stream().filter(elem -> elem.has(text(filename))) + .findFirst().ifPresent(SelenideElement::click); + } + + public void remove(String key) { + diagrams.remove(key); + } + + public boolean isDiagramExist(String key) { + String filename = getFilename(key); + return $(FolderAreaImpl.selector) + .findAll(By.className("diagrams")) + .stream().anyMatch(elem -> elem.has(text(filename))); + } + + /** + * Diagrams can be the same but have different generated ids. + * It depends on how we restore them by opening. + * In this case, we believe that they are the same. + * So we get rid of bad ids. + */ + private Element prepareElement(Element element) { + removeByAttr(element, "id", el -> el.attr("id").startsWith("j_") + || el.attr("id").startsWith("v_") + || el.attr("id").startsWith("v-") + ); + removeByAttr(element, "model-id"); + removeByAttr(element, "data-id"); + removeByAttr(element, "port", el -> el.attr("port").startsWith("out")); + removeByAttr(element, "style", el -> el.attr("style").contains("pointer-events:")); + element.getAllElements().stream() + .filter(el -> el.className().startsWith("Ports-")) + .forEach(el -> el.removeAttr("class")); + + element.getAllElements().stream() + .filter(el -> el.className().contains("selected")) + .forEach(el -> el.attr("class", el.className().replace(" selected", ""))); + + Elements sorted = new Elements( + Arrays.stream(element.toString().split("\n")) + .sorted() + .map(Element::new) + .collect(Collectors.toList()) + ); + return new Element(sorted.outerHtml()); + } + + private void removeByAttr(Element element, String attr, Predicate cond) { + element.getElementsByAttribute(attr).stream().filter(cond).forEach(el -> el.removeAttr(attr)); + } + + private void removeByAttr(Element element, String attr) { + element.getElementsByAttribute(attr).forEach(el -> el.removeAttr(attr)); + } + + private String getFilename(String path) { + String steps[] = path.split("/"); + return steps[steps.length - 1]; + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/EditorHeaderPanel.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/EditorHeaderPanel.java new file mode 100644 index 00000000..fc4fc18e --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/EditorHeaderPanel.java @@ -0,0 +1,23 @@ +package com.qreal.wmp.uitesting.headerpanel; + +import com.qreal.wmp.uitesting.headerpanel.folderwindow.FolderArea; +import com.qreal.wmp.uitesting.pages.DashboardPage; + +public interface EditorHeaderPanel { + + DashboardPage toDashboard(); + + void newDiagram(); + + FolderArea getFolderArea(); + + void saveDiagram(String path); + + void saveDiagram(); + + void openDiagram(String path); + + boolean isDiagramExist(String path); + + boolean equalsDiagrams(String path); +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/EditorHeaderPanelImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/EditorHeaderPanelImpl.java new file mode 100644 index 00000000..85df6f58 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/EditorHeaderPanelImpl.java @@ -0,0 +1,105 @@ +package com.qreal.wmp.uitesting.headerpanel; + +import com.qreal.wmp.uitesting.PageFactory; +import com.qreal.wmp.uitesting.headerpanel.folderwindow.FileItem; +import com.qreal.wmp.uitesting.headerpanel.folderwindow.FolderArea; +import com.qreal.wmp.uitesting.pages.DashboardPage; +import com.qreal.wmp.uitesting.pages.EventProvider; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +import static com.codeborne.selenide.Selectors.withText; +import static com.codeborne.selenide.Selenide.$; + +/** {@inheritDoc} */ +public class EditorHeaderPanelImpl implements EditorHeaderPanel { + + public static final By selector = By.id("main-toolbar-area"); + + private static final Logger logger = LoggerFactory.getLogger(EditorHeaderPanel.class); + + private final FileItem fileItem; + + private final PageFactory pageFactory; + + private final DiagramStoreService service; + + private final EventProvider eventProvider; + + private EditorHeaderPanelImpl(PageFactory pageFactory, WebDriver webDriver, EventProvider eventProvider) { + service = new DiagramStoreService(); + fileItem = new FileItem(webDriver); + this.pageFactory = pageFactory; + this.eventProvider = eventProvider; + } + + @Override + public DashboardPage toDashboard() { + $(selector).find(withText("Dashboard")).click(); + logger.info("Open dashboard"); + return pageFactory.getDashboardPage(); + } + + @Override + public void newDiagram() { + clickFile().newDiagram(); + eventProvider.resetEvent(); + logger.info("New diagram"); + } + + @Override + public FolderArea getFolderArea() { + return clickFile().getSaveItem(); + } + + @Override + public void saveDiagram(String path) { + moveByFolderArea(clickFile().getSaveItem(), path); + service.saveDiagram(path); + logger.info("Save diagram {}", path); + } + + @Override + public void saveDiagram() { + clickFile().saveDiagram(); + service.saveDiagram(); + } + + @Override + public void openDiagram(String path) { + moveByFolderArea(clickFile().getOpenItem(), path); + service.openDiagram(path); + logger.info("Open diagram {}", path); + } + + @Override + public boolean isDiagramExist(String path) { + moveByFolderArea(clickFile().getSaveItem(), path); + return service.isDiagramExist(path); + } + + @Override + public boolean equalsDiagrams(String path) { + return service.equalsDrigrams(path); + } + + public static EditorHeaderPanel getEditorHeaderPanel(PageFactory pageFactory, + WebDriver driver, + EventProvider eventProvider) { + return new EditorHeaderPanelImpl(pageFactory, driver, eventProvider); + } + + private FileItem clickFile() { + $(FileItem.selector).click(); + return fileItem; + } + + private void moveByFolderArea(FolderArea folderArea, String path) { + String[] steps = path.split("/"); + folderArea.move(String.join("/", Arrays.copyOf(steps, steps.length - 1))); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FileItem.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FileItem.java new file mode 100644 index 00000000..12b681b6 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FileItem.java @@ -0,0 +1,49 @@ +package com.qreal.wmp.uitesting.headerpanel.folderwindow; + +import com.codeborne.selenide.Condition; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import static com.codeborne.selenide.Selectors.withText; +import static com.codeborne.selenide.Selenide.$; + +/** Describes file item on the header menu. */ +public class FileItem { + + public static final By selector = By.id("file-menu"); + + private final FolderArea folderArea; + + private final WebDriver driver; + + public FileItem(WebDriver driver) { + this.driver = driver; + folderArea = new FolderAreaImpl(driver); + } + + /** Corresponds 'New' button. */ + public void newDiagram() { + $(selector).find(withText("New")).click(); + $(SaveDiagramConfirm.selector).waitUntil(Condition.visible, 10000); + SaveDiagramConfirm.getSaveDiagramConfirm(driver).notSave(); + } + + /** Returns folder window by clicking 'SaveAs'. */ + public FolderArea getSaveItem() { + $(selector).find(withText("SaveAs")).click(); + $(FolderAreaImpl.selector).waitUntil(Condition.visible, 10000); + return folderArea; + } + + /** Returns folder window by clicking 'Open'. */ + public FolderArea getOpenItem() { + $(selector).find(withText("Open")).click(); + $(FolderAreaImpl.selector).waitUntil(Condition.visible, 10000); + return folderArea; + } + + /** Corresponds 'Save' button. */ + public void saveDiagram() { + $(selector).find(withText("Save")).click(); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FolderArea.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FolderArea.java new file mode 100644 index 00000000..9b7e04e8 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FolderArea.java @@ -0,0 +1,18 @@ +package com.qreal.wmp.uitesting.headerpanel.folderwindow; + +/** Provides interface to working with folder window. */ +public interface FolderArea extends AutoCloseable { + FolderArea createFolder(String name); + + boolean isFolderExist(String name); + + FolderArea moveForward(String name); + + FolderArea moveBack(); + + String getCurrentPath(); + + FolderArea move(String path); + + FolderArea deleteFolder(String name); +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FolderAreaImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FolderAreaImpl.java new file mode 100644 index 00000000..55ea029f --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/FolderAreaImpl.java @@ -0,0 +1,113 @@ +package com.qreal.wmp.uitesting.headerpanel.folderwindow; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.google.common.base.Predicate; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.util.Arrays; +import java.util.function.Function; + +import static com.codeborne.selenide.Condition.text; +import static com.codeborne.selenide.Selectors.byText; +import static com.codeborne.selenide.Selenide.$; + +/** {@inheritDoc} */ +public class FolderAreaImpl implements FolderArea { + + public static final By selector = By.cssSelector("#diagrams .modal-content"); + + private final WebDriver driver; + + public FolderAreaImpl(WebDriver driver) { + this.driver = driver; + } + + @Override + public FolderArea createFolder(String folderName) { + $(selector).find(By.id("creating-menu")).click(); + $(selector).find(By.className("folder-menu")).find(By.cssSelector("[type=\"text\"]")).setValue(folderName); + $(selector).find(By.className("folder-menu")).find(By.id("creating")).click(); + if ($(selector).find(By.className("warning-message")).isDisplayed()) { + throw new IllegalArgumentException("The folder with this name already exists"); + } + return this; + } + + @Override + public boolean isFolderExist(String name) { + return $(selector).findAll(By.className("folders")).stream().anyMatch(elem -> elem.has(text(name))); + } + + @Override + public FolderArea moveForward(String name) { + if (!isFolderExist(name)) { + throw new IllegalArgumentException("Folder with name " + name + " does not exist"); + } + String oldPath = getCurrentPath(); + oldPath = "".equals(oldPath) ? name : oldPath + "/" + name; + $(selector).find(By.className("folders")).find(byText(name)).click(); + waitUntilEquals(oldPath, FolderArea::getCurrentPath); + return this; + } + + @Override + public FolderArea moveBack() { + String oldPath = getCurrentPath(); + $(selector).find(By.id("level-up")).click(); + String[] steps = oldPath.split("/"); + String diff = String.join("/", Arrays.copyOf(steps, steps.length - 1)); + waitUntilEquals(diff, FolderArea::getCurrentPath); + return this; + } + + @Override + public String getCurrentPath() { + String result = $(selector).find(By.className("folder-path")).find(By.tagName("p")).getText(); + return result.contains("/") ? result.substring(0, result.length() - 1) : result; + } + + @Override + public FolderArea move(String path) { + + while (!("").equals(getCurrentPath())) { + moveBack(); + } + Arrays.stream(path.split("/")).filter(subfolder -> !subfolder.isEmpty()).forEach(subfolder -> { + if (!isFolderExist(subfolder)) { + createFolder(subfolder); + } + moveForward(subfolder); + }); + return this; + } + + @Override + public FolderArea deleteFolder(String name) { + if (!isFolderExist(name)) { + throw new IllegalArgumentException("Folder is not exist"); + } + $(selector).find(By.className("folders")).find(byText(name)).contextClick(); + $(By.id("open-diagram-context-menu")).should(Condition.visible); + $(By.id("open-diagram-context-menu")).find(byText("Delete")).click(); + return this; + } + + @Override + public void close() { + SelenideElement closeButton = $(selector).find(By.className("close")); + if ($(selector).isDisplayed() && closeButton.isDisplayed()) { + $(selector).find(By.className("close")).click(); + } + } + + private void waitUntilEquals(String path, Function function) { + (new WebDriverWait(driver, 10)) + .until((Predicate) webDriver -> { + assert webDriver != null; + return path.equals(function.apply(this)); + }); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/SaveDiagramConfirm.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/SaveDiagramConfirm.java new file mode 100644 index 00000000..a7ec2ee1 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/headerpanel/folderwindow/SaveDiagramConfirm.java @@ -0,0 +1,34 @@ +package com.qreal.wmp.uitesting.headerpanel.folderwindow; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import static com.codeborne.selenide.Selectors.withText; +import static com.codeborne.selenide.Selenide.$; + +/** Corresponds window, which appears in case we create new diagram. */ +public class SaveDiagramConfirm { + + private final WebDriver driver; + + public static final By selector = By.id("confirm-save-diagram"); + + public SaveDiagramConfirm(WebDriver driver) { + this.driver = driver; + } + + /** 'No' button. */ + public void notSave() { + $(selector).find(withText("No")).click(); + } + + /** 'Yes' button. */ + public FolderArea save() { + $(selector).find(withText("Yes")).click(); + return new FolderAreaImpl(driver); + } + + public static SaveDiagramConfirm getSaveDiagramConfirm(WebDriver driver) { + return new SaveDiagramConfirm(driver); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/AuthPage.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/AuthPage.java new file mode 100644 index 00000000..8159d642 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/AuthPage.java @@ -0,0 +1,17 @@ +package com.qreal.wmp.uitesting.pages; + +/** Describes Authorization page of the WMP project. */ +import static com.codeborne.selenide.Selenide.title; + +public class AuthPage { + + private final String title; + + public AuthPage(String title) { + this.title = title; + } + + public boolean onPage() { + return title.equals(title()); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/DashboardPage.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/DashboardPage.java new file mode 100644 index 00000000..6159c203 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/DashboardPage.java @@ -0,0 +1,17 @@ +package com.qreal.wmp.uitesting.pages; + +import static com.codeborne.selenide.Selenide.title; + +/** Describes Dashboard page of the WMP project. */ +public class DashboardPage { + + private final String title; + + public DashboardPage(String title) { + this.title = title; + } + + public boolean onPage() { + return title().equals(title); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/EditorPage.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/EditorPage.java new file mode 100644 index 00000000..09b20a62 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/EditorPage.java @@ -0,0 +1,60 @@ +package com.qreal.wmp.uitesting.pages; + +import com.qreal.wmp.uitesting.dia.palette.Palette; +import com.qreal.wmp.uitesting.dia.property.PropertyEditor; +import com.qreal.wmp.uitesting.dia.scene.Scene; +import com.qreal.wmp.uitesting.headerpanel.EditorHeaderPanel; + +import static com.codeborne.selenide.Selenide.title; + +/** + * Describes Editor page of the WMP project. + * Includes such components as Scene, Pallete and PropertyEditor. + */ +public class EditorPage implements EventProvider.EventListener { + + private final String title; + + private final Scene scene; + + private final Palette palette; + + private final PropertyEditor propertyEditor; + + private final EditorHeaderPanel headerPanel; + + /** Describes page of the Editor and provides components. */ + public EditorPage(String title, Scene scene, Palette palette, PropertyEditor propertyEditor, + EditorHeaderPanel headerPanel) { + this.title = title; + this.scene = scene; + this.palette = palette; + this.propertyEditor = propertyEditor; + this.headerPanel = headerPanel; + } + + public Scene getScene() { + return scene; + } + + public Palette getPalette() { + return palette; + } + + public PropertyEditor getPropertyEditor() { + return propertyEditor; + } + + public EditorHeaderPanel getHeaderPanel() { + return headerPanel; + } + + public boolean onPage() { + return title.equals(title()); + } + + @Override + public void updateEvent() { + ((Resettable) scene).reset(); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/EventProvider.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/EventProvider.java new file mode 100644 index 00000000..75c28af4 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/EventProvider.java @@ -0,0 +1,31 @@ +package com.qreal.wmp.uitesting.pages; + +import java.util.ArrayList; +import java.util.List; + +/** Catches events and notify observers. */ +public class EventProvider { + + private final List listeners; + + public EventProvider() { + listeners = new ArrayList<>(); + } + + public void addListener(EventListener eventListener) { + listeners.add(eventListener); + } + + @SuppressWarnings("unused") + public void removeListener(EventListener eventListener) { + listeners.remove(eventListener); + } + + public void resetEvent() { + listeners.forEach(EventListener::updateEvent); + } + + public interface EventListener { + void updateEvent(); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/Resettable.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/Resettable.java new file mode 100644 index 00000000..31262263 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/pages/Resettable.java @@ -0,0 +1,5 @@ +package com.qreal.wmp.uitesting.pages; + +public interface Resettable { + void reset(); +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/Auther.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/Auther.java new file mode 100644 index 00000000..312f4e36 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/Auther.java @@ -0,0 +1,18 @@ +package com.qreal.wmp.uitesting.services; + +import com.qreal.wmp.uitesting.exceptions.WrongAuthException; + +/** Used for authentication in current browser session. */ +public interface Auther { + + /** + * Implements authentication to the wmp. + * + * @param username login + * @param password password + */ + void auth(final String username, final String password) throws WrongAuthException; + + /** Authentication with fixed login and password. */ + void auth() throws WrongAuthException; +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/Opener.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/Opener.java new file mode 100644 index 00000000..4ae30ae6 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/Opener.java @@ -0,0 +1,21 @@ +package com.qreal.wmp.uitesting.services; + +/** + * Used for open needed page from wmp in current browser session. + * Allows you to access as an authorized user and not. + */ +public interface Opener { + /** + * Opens page from wmp with authentication. + * + * @param page must be one of the keys from pages.property. + */ + void open(final String page); + + /** + * Opens page from wmp without authentication. + * + * @param page must be one of the keys from pages.property. + */ + void cleanOpen(final String page); +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/impl/AutherImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/impl/AutherImpl.java new file mode 100644 index 00000000..382c52bf --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/impl/AutherImpl.java @@ -0,0 +1,41 @@ +package com.qreal.wmp.uitesting.services.impl; + +import com.qreal.wmp.uitesting.exceptions.WrongAuthException; +import com.qreal.wmp.uitesting.services.Auther; +import org.openqa.selenium.By; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; + +import static com.codeborne.selenide.Selectors.byText; +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.open; + +/** {@inheritDoc} */ +public class AutherImpl implements Auther { + + /** Use properties from pages.properies file. */ + private Environment env; + + private static final Logger logger = LoggerFactory.getLogger(AutherImpl.class); + + public AutherImpl(Environment env) { + this.env = env; + } + + /** {@inheritDoc} */ + public void auth(final String username, final String password) throws WrongAuthException { + open(env.getProperty("auth")); + $(By.name("username")).setValue(username); + $(By.name("password")).setValue(password); + $("[type=\"submit\"]").click(); + if ($(byText("Password or login wrong")).exists()) { + throw new WrongAuthException(username, password); + } + logger.info("Authentication with login: {} and password: {}", username, password); + } + + public void auth() throws WrongAuthException { + auth("Admin", "Admin"); + } +} diff --git a/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/impl/OpenerImpl.java b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/impl/OpenerImpl.java new file mode 100644 index 00000000..a5ebf771 --- /dev/null +++ b/ui-testing/src/main/java/com/qreal/wmp/uitesting/services/impl/OpenerImpl.java @@ -0,0 +1,51 @@ +package com.qreal.wmp.uitesting.services.impl; + +import com.qreal.wmp.uitesting.exceptions.WrongAuthException; +import com.qreal.wmp.uitesting.services.Auther; +import com.qreal.wmp.uitesting.services.Opener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.security.access.AccessDeniedException; + +import static com.codeborne.selenide.Selectors.byText; +import static com.codeborne.selenide.Selenide.$; + +/** {@inheritDoc} */ +public class OpenerImpl implements Opener { + + /** Uses properties from pages.properies file. */ + private Environment env; + + private Auther auther; + + private static final Logger logger = LoggerFactory.getLogger(OpenerImpl.class); + + public OpenerImpl(Environment env, Auther auther) { + this.env = env; + this.auther = auther; + } + + /** {@inheritDoc} */ + public void open(final String page) { + try { + com.codeborne.selenide.Selenide.open(env.getProperty(page)); + logger.info("Open page {}", env.getProperty(page)); + if ($(byText("Sign in to continue to Auth")).exists()) { + logger.info("Fail with open page {}. Try to login.", env.getProperty(page)); + auther.auth(); + } + com.codeborne.selenide.Selenide.open(env.getProperty(page)); + logger.info("Open page {}", env.getProperty(page)); + } catch (WrongAuthException e) { + logger.error("Opener fails: " + e.getMessage()); + throw new AccessDeniedException(e.getMessage()); + } + logger.info("Open page {}", env.getProperty(page)); + } + + public void cleanOpen(final String page) { + com.codeborne.selenide.Selenide.open(env.getProperty(page)); + logger.info("Open page {}", env.getProperty(page)); + } +} diff --git a/ui-testing/src/main/resources/pages.properties b/ui-testing/src/main/resources/pages.properties new file mode 100644 index 00000000..0ffeabe3 --- /dev/null +++ b/ui-testing/src/main/resources/pages.properties @@ -0,0 +1,4 @@ +auth=http://localhost:${port.auth}${path.auth} +dashboard=http://localhost:${port.dashboard}${path.dashboard} +robotsEditor=http://localhost:${port.editor}${path.editor.robots} +bpmnEditor=http://localhost:${port.editor}${path.editor.bpmn} \ No newline at end of file diff --git a/ui-testing/src/main/resources/services.properties b/ui-testing/src/main/resources/services.properties deleted file mode 100644 index 1382b546..00000000 --- a/ui-testing/src/main/resources/services.properties +++ /dev/null @@ -1 +0,0 @@ -accessDashboardUri=http://localhost:${port.dashboard}${path.dashboard} \ No newline at end of file diff --git a/ui-testing/src/test/java/com/qreal/wmp/uitesting/auth/AuthDashboardTest.java b/ui-testing/src/test/java/com/qreal/wmp/uitesting/auth/AuthDashboardTest.java deleted file mode 100644 index b05f4547..00000000 --- a/ui-testing/src/test/java/com/qreal/wmp/uitesting/auth/AuthDashboardTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.qreal.wmp.uitesting.auth; - -import com.codeborne.selenide.WebDriverRunner; -import io.github.bonigarcia.wdm.ChromeDriverManager; -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.*; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.chrome.ChromeDriver; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -import static com.codeborne.selenide.Condition.appear; -import static com.codeborne.selenide.Condition.exist; -import static com.codeborne.selenide.Selectors.byText; -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.open; - -public class AuthDashboardTest { - - private static String dashboardUrl; - - private static WebDriver driver; - - /** - * Setup ChromeDriverManager and load correct urls from .properties file. - */ - @BeforeClass - public static void setUpClass() { - ChromeDriverManager.getInstance().setup(); - final String resourceName = "services.properties"; - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - Properties props = new Properties(); - try (InputStream resourceStream = loader.getResourceAsStream(resourceName)) { - props.load(resourceStream); - } catch (IOException e) { - e.printStackTrace(); - } - dashboardUrl = props.getProperty("accessDashboardUri"); - } - - /** - * Try to open dashboard page. - * Should be redirected to auth page. - */ - @Before - public void openAuthPage() { - driver = new ChromeDriver(); - WebDriverRunner.setWebDriver(driver); - open(dashboardUrl); - $(byText("Sign in to continue to Auth")).shouldBe(exist); - $(byText("Dashboard")).shouldNotBe(exist); - } - - /** - * Try to login with correct username and password. - * Should access and redirect to dashboard - */ - @Test - public void userCanLoginByUsername() { - $(By.name("username")).setValue("123"); - $(By.name("password")).setValue("123"); - $("[type=\"submit\"]").click(); - $(byText("Dashboard")).waitUntil(appear, 50000); - } - - /** - * Try to login with random username and password. - * An error must be shown - */ - @Test - public void userWrongAuth() { - $(byText("Password or login wrong")).shouldNotBe(exist); - char[] alphabet = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); - String wrongLogin = RandomStringUtils.random(20, alphabet); - String wrongPassword = RandomStringUtils.random(20, alphabet); - $(By.name("username")).setValue(wrongLogin); - $(By.name("password")).setValue(wrongPassword); - $("[type=\"submit\"]").click(); - $(byText("Password or login wrong")).shouldBe(exist); - } - - @After - public void logout() { - driver.close(); - } - -} diff --git a/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/AuthTest.java b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/AuthTest.java new file mode 100644 index 00000000..5759ade8 --- /dev/null +++ b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/AuthTest.java @@ -0,0 +1,124 @@ +package com.qreal.wmp.uitesting.innertests; + +import com.qreal.wmp.uitesting.config.AppInit; +import com.qreal.wmp.uitesting.exceptions.WrongAuthException; +import com.qreal.wmp.uitesting.services.Auther; +import com.qreal.wmp.uitesting.services.Opener; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import static com.codeborne.selenide.Condition.appear; +import static com.codeborne.selenide.Selectors.byText; +import static com.codeborne.selenide.Selenide.$; + +/** Tests for opener and auther services. */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppInit.class, loader = AnnotationConfigContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class AuthTest { + + private static final String WRONG_LOGIN = "lbltfn16vup5boj7o1ju"; + + private static final String WRONG_PASSWORD = "8epo7li9uq5vs3wujpm4"; + + @Autowired + private Auther auther; + + @Autowired + private Opener opener; + + /** + * Try to login with correct username and password. + * Should redirect to OAuth page. + */ + @Test + public void authTest() { + try { + opener.cleanOpen("auth"); + assert inAuthPage(); + auther.auth(); + assert $(byText("OAuth Server")).waitUntil(appear, 5000).exists(); + } catch (WrongAuthException e) { + System.err.println(e.getMessage()); + } + } + + /** + * Try to login with random username and password. + * An error must be shown. + */ + @Test + public void authWrongTest() { + opener.cleanOpen("auth"); + assert inAuthPage(); + try { + auther.auth(WRONG_LOGIN, WRONG_PASSWORD); + } catch (WrongAuthException e) { + System.err.println(e.getMessage()); + } + $(byText("Password or login wrong")).waitUntil(appear, 5000); + } + + /** + * Try to open dashboard page without authentication. + * Should be redirected to auth page. + * Try to open dashboard page with correct login and password. + */ + @Test + public void dashboardTest() { + opener.cleanOpen("dashboard"); + assert inAuthPage(); + opener.open("dashboard"); + $(byText("Dashboard")).waitUntil(appear, 5000); + } + + /** + * Try to open robots-editor page without authentication. + * Should be redirected to auth page. + * Try to open robots-editor page with correct login and password. + */ + @Test + public void robotsEditorTest() { + opener.cleanOpen("robotsEditor"); + assert inAuthPage(); + opener.open("robotsEditor"); + $(byText("Property Editor")).waitUntil(appear, 5000); + } + + /** + * Try to open bpmn-editor page without authentication. + * Should be redirected to auth page. + * Try to open bpmn-editor page with correct login and password. + */ + @Test + public void bpmnEditorTest() { + opener.cleanOpen("bpmnEditor"); + assert inAuthPage(); + opener.open("bpmnEditor"); + $(byText("Property Editor")).waitUntil(appear, 5000); + } + + /** + * Check that current page is Auth page. + * + * @return true if it is + */ + private boolean inAuthPage() { + return $(byText("Sign in to continue to Auth")).exists(); + } + + /** To generate random login and password. */ + public static void main(String[] args) { + final char[] alphabet = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); + final String wrongLogin = RandomStringUtils.random(20, alphabet); + final String wrongPassword = RandomStringUtils.random(20, alphabet); + System.out.println(wrongLogin); + System.out.println(wrongPassword); + } +} diff --git a/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/FolderAreaTest.java b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/FolderAreaTest.java new file mode 100644 index 00000000..f9650d90 --- /dev/null +++ b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/FolderAreaTest.java @@ -0,0 +1,148 @@ +package com.qreal.wmp.uitesting.innertests; + +import com.qreal.wmp.uitesting.Page; +import com.qreal.wmp.uitesting.PageLoader; +import com.qreal.wmp.uitesting.config.AppInit; +import com.qreal.wmp.uitesting.dia.palette.Palette; +import com.qreal.wmp.uitesting.dia.scene.Scene; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.Link; +import com.qreal.wmp.uitesting.headerpanel.EditorHeaderPanel; +import com.qreal.wmp.uitesting.headerpanel.folderwindow.FolderArea; +import com.qreal.wmp.uitesting.pages.DashboardPage; +import com.qreal.wmp.uitesting.pages.EditorPage; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppInit.class, loader = AnnotationConfigContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class FolderAreaTest { + + private static final Logger logger = LoggerFactory.getLogger(FolderAreaTest.class); + + @Autowired + private PageLoader pageLoader; + + private Scene scene; + + private Palette pallete; + + private EditorHeaderPanel headerPanel; + + private final char[] alphabet = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); + + @Before + public void openEditor() { + EditorPage editorPage = pageLoader.load(Page.EditorRobots); + scene = editorPage.getScene(); + pallete = editorPage.getPalette(); + headerPanel = editorPage.getHeaderPanel(); + } + + @Test + public void newDiagramTest() { + + List elements = new ArrayList<>(); + List links = new ArrayList<>(); + + elements.add(scene.dragAndDrop(pallete.getElement("Initial Node"), 4, 4)); + elements.add(scene.dragAndDrop(pallete.getElement("Motors Forward"), 10, 4)); + links.add(scene.addLink(elements.get(0), elements.get(1))); + elements.add(scene.dragAndDrop(pallete.getElement("Painter Color"), 16, 4)); + links.add(scene.addLink(elements.get(1), elements.get(2))); + + headerPanel.newDiagram(); + assert elements.stream().noneMatch(x -> scene.exist(x)); + assert links.stream().noneMatch(x -> scene.exist(x)); + } + + @Test + public void clickDashboardTest() { + DashboardPage dashboardPage = headerPanel.toDashboard(); + assert dashboardPage.onPage(); + } + + @Test + public void createFolderTest() { + final String folder = RandomStringUtils.random(10, alphabet); + try (FolderArea folderArea = headerPanel.getFolderArea()) { + folderArea.createFolder(folder); + assert folderArea.isFolderExist(folder); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void warnIsFolderExistTest() throws Exception { + final String folder = RandomStringUtils.random(10, alphabet); + try (FolderArea folderArea = headerPanel.getFolderArea()) { + folderArea.createFolder(folder).createFolder(folder); + } + } + + @Test + public void moveForwardFolderTest() { + final String folder = RandomStringUtils.random(10, alphabet); + try (FolderArea folderArea = headerPanel.getFolderArea()) { + Path oldPath = Paths.get("/" + folderArea.getCurrentPath()); + Path newPath = Paths.get("/" + folderArea.createFolder(folder).moveForward(folder).getCurrentPath()); + assert newPath.startsWith(oldPath); + assert newPath.getFileName().toString().equals(folder); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } + + @Test + public void moveBackFolderTest() { + final String folder = RandomStringUtils.random(10, alphabet); + try (FolderArea folderArea = headerPanel.getFolderArea()) { + folderArea.createFolder(folder); + Path oldPath = Paths.get("/" + folderArea.moveForward(folder).getCurrentPath()); + Path newPath = Paths.get("/" + folderArea.moveBack().getCurrentPath()); + assert oldPath.startsWith(newPath); + assert oldPath.getFileName().toString().equals(folder); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } + + @Test + public void moveFolderTest() { + final String folder = RandomStringUtils.random(10, alphabet) + + "/" + RandomStringUtils.random(10, alphabet); + try (FolderArea folderArea = headerPanel.getFolderArea()) { + folderArea.move(folder); + assert folderArea.getCurrentPath().equals(folder); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } + + @Test + public void deleteFolderTest() { + final String folder = RandomStringUtils.random(10, alphabet); + try (FolderArea folderArea = headerPanel.getFolderArea()) { + folderArea.createFolder(folder).deleteFolder(folder); + assert !folderArea.isFolderExist(folder); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } +} diff --git a/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/ManipulatingDiagramTest.java b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/ManipulatingDiagramTest.java new file mode 100644 index 00000000..407b2209 --- /dev/null +++ b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/ManipulatingDiagramTest.java @@ -0,0 +1,112 @@ +package com.qreal.wmp.uitesting.innertests; + +import com.qreal.wmp.uitesting.Page; +import com.qreal.wmp.uitesting.PageLoader; +import com.qreal.wmp.uitesting.config.AppInit; +import com.qreal.wmp.uitesting.dia.palette.Palette; +import com.qreal.wmp.uitesting.dia.property.PropertyEditor; +import com.qreal.wmp.uitesting.dia.scene.Scene; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.Link; +import com.qreal.wmp.uitesting.exceptions.ElementNotOnTheSceneException; +import com.qreal.wmp.uitesting.pages.EditorPage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppInit.class, loader = AnnotationConfigContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class ManipulatingDiagramTest { + + private static final Logger logger = LoggerFactory.getLogger(ManipulatingDiagramTest.class); + + @Autowired + private PageLoader pageLoader; + + private Palette palette; + + private Scene scene; + + private PropertyEditor propertyEditor; + + /** Open editor page. */ + @Before + public void openEditor() { + EditorPage editorPage = pageLoader.load(Page.EditorRobots); + scene = editorPage.getScene(); + palette = editorPage.getPalette(); + propertyEditor = editorPage.getPropertyEditor(); + } + + /** Drag element from palette and drop on the scene. */ + @Test + public void dragAndDrop() { + final Block initialNode = scene.dragAndDrop(palette.getElement("Initial Node")); + assert scene.exist(initialNode); + } + + /** Remove element from scene. */ + @Test + public void remove() { + final Block sceneElement = scene.dragAndDrop(palette.getElement("Initial Node")); + assert scene.exist(sceneElement); + try { + scene.remove(sceneElement); + } catch (ElementNotOnTheSceneException e) { + logger.error(e.getMessage()); + } + assert !scene.exist(sceneElement); + } + + /** Add two elements and link them. */ + @Test + public void addLink() { + final Block initNode = scene.dragAndDrop(palette.getElement("Initial Node"), 4, 4); + final Block finalNode = scene.dragAndDrop(palette.getElement("Final Node"), 4, 70); + final Block motor = scene.dragAndDrop(palette.getElement("Motors Forward"), 4, 7); + Link link = scene.addLink(initNode, motor); + Link link2 = scene.addLink(motor, finalNode); + motor.moveToCell(72, 64); + assert scene.exist(link); + assert scene.exist(link2); + } + + /** Set property 'Ports' of motor forward item to '123' and checks that all is correct. */ + @Test + public void propertyEditor() { + final Block motor = scene.dragAndDrop(palette.getElement("Motors Forward")); + propertyEditor.setProperty(motor.getInnerSeleniumElement(), "Ports", "123"); + assert propertyEditor.getProperty(motor.getInnerSeleniumElement(), "Ports").equals("123"); + } + + /** Move element to cell. */ + @Test + public void moveElement() { + final Block motor = scene.dragAndDrop(palette.getElement("Motors Forward")); + try { + motor.moveToCell(40, 40); + assert motor.getCoordinateOnScene().getXCell() == 40 && motor.getCoordinateOnScene().getYCell() == 40; + motor.moveToCell(72, 64); + assert motor.getCoordinateOnScene().getXCell() == 72 && motor.getCoordinateOnScene().getYCell() == 64; + motor.moveToCell(0, 0); + assert motor.getCoordinateOnScene().getXCell() == 0 && motor.getCoordinateOnScene().getYCell() == 0; + } catch (ElementNotOnTheSceneException e) { + logger.error(e.getMessage()); + } + } + + /** Clean scene. */ + @After + public void cleanScene() { + scene.clean(); + } +} diff --git a/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/SaveOpenDiagramTest.java b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/SaveOpenDiagramTest.java new file mode 100644 index 00000000..f3df314e --- /dev/null +++ b/ui-testing/src/test/java/com/qreal/wmp/uitesting/innertests/SaveOpenDiagramTest.java @@ -0,0 +1,106 @@ +package com.qreal.wmp.uitesting.innertests; + +import com.qreal.wmp.uitesting.Page; +import com.qreal.wmp.uitesting.PageLoader; +import com.qreal.wmp.uitesting.config.AppInit; +import com.qreal.wmp.uitesting.dia.palette.Palette; +import com.qreal.wmp.uitesting.dia.scene.Scene; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.Link; +import com.qreal.wmp.uitesting.headerpanel.EditorHeaderPanel; +import com.qreal.wmp.uitesting.pages.EditorPage; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppInit.class, loader = AnnotationConfigContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class SaveOpenDiagramTest { + + @Autowired + private PageLoader pageLoader; + + private Scene scene; + + private Palette palette; + + private EditorHeaderPanel headerPanel; + + private final char[] alphabet = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); + + private final List elements = new ArrayList<>(); + + private final List links = new ArrayList<>(); + + private String diagram; + + /** Opens editor page and add initial set of elements. */ + @Before + public void openEditor() { + EditorPage editorPage = pageLoader.load(Page.EditorRobots); + scene = editorPage.getScene(); + palette = editorPage.getPalette(); + headerPanel = editorPage.getHeaderPanel(); + addElements(); + diagram = RandomStringUtils.random(10, alphabet); + headerPanel.saveDiagram(diagram); + } + + @Test + public void saveDiagramTest() { + assert headerPanel.isDiagramExist(diagram); + } + + /* + @Test + public void openDiagramTest() { + headerPanel.newDiagram(); + headerPanel.openDiagram(diagram); + assert headerPanel.equalsDiagrams(diagram); + } + */ + + @Test + public void equalsTrueTest() { + headerPanel.newDiagram(); + addElements(); + assert headerPanel.equalsDiagrams(diagram); + } + + @Test + public void equalsFalseTest() { + headerPanel.newDiagram(); + addElements(); + scene.moveToCell(elements.get(0), 10, 10); + assert !headerPanel.equalsDiagrams(diagram); + } + + @Test + public void saveAfterChangesTest() { + scene.clean(); + addElements(); + scene.moveToCell(elements.get(0), 10, 10); + headerPanel.saveDiagram(); + assert headerPanel.equalsDiagrams(diagram); + } + + private void addElements() { + elements.clear(); + links.clear(); + elements.add(scene.dragAndDrop(palette.getElement("Initial Node"), 4, 4)); + elements.add(scene.dragAndDrop(palette.getElement("Motors Forward"), 10, 4)); + links.add(scene.addLink(elements.get(0), elements.get(1))); + elements.add(scene.dragAndDrop(palette.getElement("Painter Color"), 16, 4)); + links.add(scene.addLink(elements.get(1), elements.get(2))); + } +} diff --git a/ui-testing/src/test/java/com/qreal/wmp/uitesting/testspace/DiagramConstructingTest.java b/ui-testing/src/test/java/com/qreal/wmp/uitesting/testspace/DiagramConstructingTest.java new file mode 100644 index 00000000..cb68505a --- /dev/null +++ b/ui-testing/src/test/java/com/qreal/wmp/uitesting/testspace/DiagramConstructingTest.java @@ -0,0 +1,95 @@ +package com.qreal.wmp.uitesting.testspace; + +import com.qreal.wmp.uitesting.Page; +import com.qreal.wmp.uitesting.PageLoader; +import com.qreal.wmp.uitesting.config.AppInit; +import com.qreal.wmp.uitesting.dia.palette.Palette; +import com.qreal.wmp.uitesting.dia.property.PropertyEditor; +import com.qreal.wmp.uitesting.dia.scene.Scene; +import com.qreal.wmp.uitesting.dia.scene.elements.Block; +import com.qreal.wmp.uitesting.dia.scene.elements.Link; +import com.qreal.wmp.uitesting.pages.EditorPage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import java.util.ArrayList; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppInit.class, loader = AnnotationConfigContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class DiagramConstructingTest { + + @Autowired + private PageLoader pageLoader; + + private Scene scene; + + private PropertyEditor propertyEditor; + + private ArrayList elements; + + private ArrayList links; + + /** Open editor page. */ + @Before + public void openEditor() { + EditorPage editorPage = pageLoader.load(Page.EditorRobots); + scene = editorPage.getScene(); + Palette palette = editorPage.getPalette(); + propertyEditor = editorPage.getPropertyEditor(); + + elements = new ArrayList<>(); + links = new ArrayList<>(); + + elements.add(scene.dragAndDrop(palette.getElement("Initial Node"), 4, 4)); + elements.add(scene.dragAndDrop(palette.getElement("Motors Forward"), 10, 4)); + links.add(scene.addLink(elements.get(0), elements.get(1))); + elements.add(scene.dragAndDrop(palette.getElement("Painter Color"), 16, 4)); + links.add(scene.addLink(elements.get(1), elements.get(2))); + elements.add(scene.dragAndDrop(palette.getElement("Timer"), 22, 4)); + links.add(scene.addLink(elements.get(2), elements.get(3))); + elements.add(scene.dragAndDrop(palette.getElement("Final Node"), 28, 4)); + links.add(scene.addLink(elements.get(3), elements.get(4))); + } + + @Test + public void digramFiveNodes() { + assert allExist(); + } + + @Test + public void moveSomeNodes() { + elements.get(1).moveToCell(20, 20); + elements.get(0).moveToCell(20, 10); + elements.get(1).moveToCell(0, 20); + assert allExist(); + } + + @Test + public void fillProperties() { + propertyEditor.setProperty(elements.get(1).getInnerSeleniumElement(), "Power", "80"); + assert propertyEditor.getProperty(elements.get(1).getInnerSeleniumElement(), "Power").equals("80"); + propertyEditor.setProperty(elements.get(2).getInnerSeleniumElement(), "Color", "green"); + assert propertyEditor.getProperty(elements.get(2).getInnerSeleniumElement(), "Color").equals("green"); + propertyEditor.setProperty(elements.get(3).getInnerSeleniumElement(), "Delay", "200"); + assert propertyEditor.getProperty(elements.get(3).getInnerSeleniumElement(), "Delay").equals("200"); + } + + /** Clean scene. */ + @After + public void cleanScene() { + scene.clean(); + } + + private boolean allExist() { + return elements.stream().allMatch(scene::exist) && links.stream().anyMatch(scene::exist); + } + +} diff --git a/ui-testing/ui-testing.iml b/ui-testing/ui-testing.iml index fe03b019..c8edc642 100644 --- a/ui-testing/ui-testing.iml +++ b/ui-testing/ui-testing.iml @@ -1,5 +1,14 @@ - + + + + + + file://$MODULE_DIR$/src/main/java/com/qreal/wmp/uitesting/config/DevConfig.java + + + + @@ -12,52 +21,70 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +