Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ jobs:
- name: Prepare Maven Wrapper
run: chmod +x ./mvnw

# install is required so the install-browsers step finds all the other modules
- name: Build with Maven
run: ./mvnw clean verify -U -B -ntp -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120
run: ./mvnw clean install -U -B -ntp -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120

- name: Prepare I-Tests
run: mkdir -p target/jgiven-reports/json

- name: Install Browsers for Playwright
run: ./mvnw exec:java -B -e -f scenarios/single-node-jpa -Dexec.classpathScope="test" -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps"

- name: Run I-Tests with Maven
run: ./mvnw integration-test failsafe:verify -Pitest -DskipFrontend -B -ntp
run: ./mvnw integration-test failsafe:verify -Pitest -B -ntp

- name: Upload coverage information
uses: codecov/codecov-action@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ <h6>{{ task.description }}</h6>
</div>
<div class="col-lg-12 description">
<form #approvalForm="ngForm" novalidate>
<input type="radio" required name="approvalDecision" value="APPROVE" label="Approve request" [(ngModel)]="submitData.decision">&nbsp;Approve request<br/>
<input type="radio" name="approvalDecision" value="REJECT" label="Reject equestr" [(ngModel)]="submitData.decision">&nbsp;Reject request<br/>
<input type="radio" name="approvalDecision" value="RETURN" label="Return to origintaor" [(ngModel)]="submitData.decision">&nbsp;Return request to originator<br/>
<label>
<input type="radio" required name="approvalDecision" value="APPROVE" [(ngModel)]="submitData.decision">&nbsp;Approve request
</label><br/>
<label><input type="radio" name="approvalDecision" value="REJECT" [(ngModel)]="submitData.decision">&nbsp;Reject request</label><br/>
<label><input type="radio" name="approvalDecision" value="RETURN" [(ngModel)]="submitData.decision">&nbsp;Return request to originator</label><br/>

<div class="buttons">
<button [disabled]="!approvalForm.valid" class="btn btn-primary" type="button" (click)="complete()">Complete</button>&nbsp;
Expand Down
2 changes: 1 addition & 1 deletion components/tasklist-angular/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<div class="flex-spacer"></div>

<div class="navbar-nav">
<div class="navbar-nav flex-grow-1 justify-content-between">
<tasks-search></tasks-search>
<tasks-user-selection></tasks-user-selection>
</div>
Expand Down
23 changes: 8 additions & 15 deletions components/tasklist-angular/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { Component, OnInit, inject } from '@angular/core';
import {UserStoreService} from 'app/user/state/user.store-service';
import { HeaderComponent } from './header/header.component';
import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { ProcesslistComponent } from './process/process-list/process-list.component';
import { SearchComponent } from './search/search.component';
import { UserSelectionComponent } from './user/user-selection/user-selection.component';
import { FooterComponent } from './footer/footer.component';
import {Component} from '@angular/core';
import {HeaderComponent} from './header/header.component';
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {ProcesslistComponent} from './process/process-list/process-list.component';
import {SearchComponent} from './search/search.component';
import {UserSelectionComponent} from './user/user-selection/user-selection.component';
import {FooterComponent} from './footer/footer.component';

@Component({
selector: 'tasks-root',
templateUrl: './app.component.html',
styleUrls: ['app.component.scss'],
imports: [HeaderComponent, RouterLinkActive, RouterLink, ProcesslistComponent, SearchComponent, UserSelectionComponent, RouterOutlet, FooterComponent]
})
export class AppComponent implements OnInit {
private userStore = inject(UserStoreService);


ngOnInit(): void {
this.userStore.loadInitialUser();
}
export class AppComponent {
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<div class="nav-item dropdown" ngbDropdown>
<div class="nav-link dropdown-toggle" ngbDropdownToggle>Start new...</div>
<button class="btn btn-outline-primary dropdown-toggle" ngbDropdownToggle>Start Process...</button>

<div class="dropdown-menu" ngbDropdownMenu>
<ul class="dropdown-menu" ngbDropdownMenu aria-label="Startable Processes Models">
@for (process of (processes$ | async); track process.url) {
<a
<li><a
tasksExternalUrl
class="dropdown-item"
data-toggle="tooltip"
data-placement="bottom"
title="{{ process.description }}"
[href]="process.url"
>{{ process.name }}</a>
>{{ process.name }}</a></li>
}
</div>
</ul>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<form class="form-inline">
<input class="form-control form-control-sm mr-sm-2 mb-0" type="text" placeholder="Search" aria-label="Search">
<button class="form-control btn btn-sm btn-outline-secondary" type="submit">Search</button>
<form class="d-flex gap-2">
<input class="form-control form-control-sm mb-0" type="text" name="search" placeholder="Search" aria-label="Search">
<button class="btn btn-sm btn-outline-secondary" type="submit">Search</button>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class TaskEffects {
loadTasks$ = createEffect(() => this.actions$.pipe(
ofType(loadTasks),
withLatestFrom(this.userStore.userId$),
filter(([, userId]) => !!userId),
mergeMap(([, userId]) =>
this.taskService.getTasks$Response({
'X-Current-User-ID': userId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<table class="table">
<table class="table" aria-label="Open Tasks">
<thead class="thead-light">
<tr>
<th class="align-top" tasks-sortable-column="task.process">Process</th>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { first } from 'rxjs/operators';
import { loadAvailableUsers, selectUser } from './user.actions';
import { availableUsers, currentUserId, currentUserProfile, StateWithUsers } from './user.selectors';

Expand All @@ -24,8 +23,4 @@ export class UserStoreService {
selectUser(userId: string) {
this.store.dispatch(selectUser({ userId }))
}

loadInitialUser(): void {
this.userId$.pipe(first()).subscribe(userId => this.selectUser(userId));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<div class="nav-item dropdown avatar" ngbDropdown>
<div class="nav-link dropdown-toggle" ngbDropdownToggle>
<div class="btn-group" ngbDropdown>
<button class="btn btn-light" disabled>
<i class="fa fa-user" aria-label="current user:"></i>
{{ (currentProfile$ | async).fullName }}
</button>
<button class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" ngbDropdownToggle>
<!--
<img src="{{ user.avatar }}" class="rounded-circle z-depth-0" alt="avatar image" height="35">
-->
<i class="fa fa-user"></i>
{{ (currentProfile$ | async).fullName }}
</div>
<ul class="dropdown-menu" ngbDropdownMenu>
<span class="visually-hidden">Change user</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" ngbDropdownMenu aria-label="available users">

@for (user of (availableUsers$ | async); track user) {
<li><button class="dropdown-item" (click)="setCurrentUser(user.id)">{{ user.username }}</button></li>
Expand Down
14 changes: 12 additions & 2 deletions components/tasklist-angular/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
import {bootstrapApplication} from '@angular/platform-browser';
import {provideAnimations} from '@angular/platform-browser/animations';
import {externalUrlProvider, externalUrlProviderActivateGuard, routes} from 'app/app-routing.module';
import {provideState, provideStore} from '@ngrx/store';
import {ActionReducer, provideState, provideStore} from '@ngrx/store';
import {storePersist} from 'app/store-persist';
import {provideEffects} from '@ngrx/effects';
import {provideStoreDevtools} from '@ngrx/store-devtools';
Expand All @@ -30,6 +30,16 @@ if (environment.production) {
enableProdMode();
}

/**
* Helper for debugging dispatched actions during java-based integration tests
* */
const logActions = (reducer: ActionReducer<unknown>): ActionReducer<unknown> => {
return (state, action) => {
console.log(action)
return reducer(state, action);
}
}

bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
Expand All @@ -48,7 +58,7 @@ bootstrapApplication(AppComponent, {
provideHttpClient(withInterceptorsFromDi()),
// ngrx store
provideStore({}, {
metaReducers: [storePersist],
metaReducers: [storePersist, logActions],
runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true }
}),
provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
Expand Down
7 changes: 7 additions & 0 deletions scenarios/single-node-jpa/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@
<groupId>io.holunda.polyflow</groupId>
<artifactId>polyflow-example-infrastructure</artifactId>
</dependency>
<!-- Smoke Tests -->
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.54.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.holunda.polyflow.example.process.approval

import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.ActiveProfiles

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("itest", "jpa")
abstract class AbstractIT {

@Configuration
class TestConfiguration {

@Bean
fun taskUrlResolver() =
TestingUrlResolver()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.holunda.polyflow.example.process.approval

import com.microsoft.playwright.BrowserContext
import com.microsoft.playwright.Page
import com.microsoft.playwright.Tracing.StartOptions
import com.microsoft.playwright.Tracing.StopOptions
import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat
import com.microsoft.playwright.junit.UsePlaywright
import com.microsoft.playwright.options.AriaRole
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.boot.test.web.server.LocalServerPort
import java.nio.file.Paths


@UsePlaywright
class FrontendIT: AbstractIT() {

@LocalServerPort
private var port: Int = 0

@BeforeEach
fun setUp(context: BrowserContext) {
// Start tracing before creating / navigating a page.
context.tracing().start(
StartOptions()
.setScreenshots(true)
.setSnapshots(true)
.setSources(true)
)
}

@AfterEach
fun tearDown(context: BrowserContext) {
// Stop tracing and export it into a zip archive.
context.tracing().stop(
StopOptions()
.setPath(Paths.get("trace.zip"))
)
}

@Test
fun `should start application`(page: Page) {

page.navigate("http://localhost:${port}/polyflow")

// Tasklist SPA is being served correctly
assertThat(page).hasTitle("POLYFLOW Process Platform")

// process definitions are being shown
val startNewProcessButton = page.getByText("Start Process...")
assertThat(startNewProcessButton).hasRole(AriaRole.BUTTON)
startNewProcessButton.click()
val processList = page.getByLabel("Startable Processes Models")
assertThat(processList).isInViewport()
assertThat(processList.getByText("Request Approval")).isInViewport()

// change user to see tasks on the default started process
page.getByText("Change User").click()
page.getByLabel("Available Users").getByText("fozzy").click()
assertThat(page.getByRole(AriaRole.BUTTON, Page.GetByRoleOptions().setName("current user:"))).hasText("Fozzy");

// there is a task in the list
assertThat(page.getByLabel("Open Tasks")).hasText("Please approve request .* from kermit on behalf of piggy".toPattern())

// go to task
val taskLink = page.getByRole(AriaRole.LINK, Page.GetByRoleOptions().setName("Approve Request"))
taskLink.click()

// Example Process SPA is being served correctly
assertThat(page).hasTitle("Example process approval")

assertThat(page.getByText("Approval Request")).isInViewport()

// complete task
page.getByRole(AriaRole.RADIO, Page.GetByRoleOptions().setName("Approve request")).check()
page.getByRole(AriaRole.BUTTON, Page.GetByRoleOptions().setName("Complete")).click()

// tasklist is shown again, now empty
val taskList = page.getByLabel("Open Tasks")
assertThat(taskList).isInViewport()
assertThat(taskList).containsText("No tasks");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles

@SpringBootTest
@ActiveProfiles("itest", "jpa")
class SingleNodeScenarioContextStartIT {
class SingleNodeScenarioContextStartIT : AbstractIT() {

@Autowired
lateinit var userService: UserService
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.holunda.polyflow.example.process.approval

import io.holunda.polyflow.urlresolver.TasklistUrlResolver
import io.holunda.polyflow.view.DataEntry
import io.holunda.polyflow.view.FormUrlResolver
import io.holunda.polyflow.view.ProcessDefinition
import io.holunda.polyflow.view.Task
import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
import org.springframework.context.event.EventListener

/**
* Override config based url resolvers, because the WebEnvironment.RANDOM_PORT cannot be resolved inside configurations.
*/
class TestingUrlResolver : FormUrlResolver, TasklistUrlResolver {

private var port: Int = 0

/**
* The only way to get the random port at runtime.
* See: https://www.baeldung.com/spring-boot-running-port#2-handling-servletwebserverinitializedevent
*/
@EventListener
fun onApplicationEvent(event: ServletWebServerInitializedEvent) {
port = event.webServer.port
}

override fun resolveUrl(task: Task): String {
return "http://localhost:$port/example-process-approval/tasks/${task.formKey}/${task.id}?userId=%userId%"
}

override fun resolveUrl(processDefinition: ProcessDefinition): String {
return "http://localhost:$port/example-process-approval/${processDefinition.formKey}?userId=%userId%"
}

override fun resolveUrl(dataEntry: DataEntry): String {
return "http://localhost:$port/example-process-approval/approval-request/${dataEntry.entryId}?userId=%userId%"
}

override fun getTasklistUrl(): String {
return "http://localhost:$port/polyflow/tasks"
}
}