Initial ProgressView implementation

- Provides a basic feature set for ProgressView.
- Progress can have a set of items which are driven
  by function style factory interface.
- New class TextCell which is a base of progress items.
- Not yet fully working and further changes, samples, docs
  will come in other commits.
- Relates #995
This commit is contained in:
Janne Valkealahti
2024-02-02 08:44:28 +00:00
parent 3d52f174af
commit 10d9940299
6 changed files with 1133 additions and 1 deletions

View File

@@ -0,0 +1,439 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.view.control;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import org.springframework.shell.component.message.ShellMessageBuilder;
import org.springframework.shell.component.view.control.cell.TextCell;
import org.springframework.shell.component.view.screen.Screen;
import org.springframework.shell.geom.HorizontalAlign;
import org.springframework.shell.geom.Rectangle;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.util.Assert;
/**
* {@code ProgressView} is used to show a progress indicator.
*
* Defaults to <textItem> <spinnerItem> <percentItem>
*
* @author Janne Valkealahti
*/
public class ProgressView extends BoxView {
private final int tickStart;
private final int tickEnd;
private int tickValue;
private boolean running = false;
private String description;
private Spinner spinner = Spinner.of(Spinner.LINE1, 130);
private int spinnerFrame;
private List<ProgressViewItem> items;
private GridView grid;
private final static Function<Context, TextCell<Context>> DEFAULT_DESCRIPTION_FACTORY =
(item) -> TextCell.of(item, ctx -> {
return ctx.getDescription();
});
private final static Function<Context, TextCell<Context>> DEFAULT_PERCENT_FACTORY =
(item) -> {
TextCell<Context> cell = TextCell.of(item, ctx -> {
ProgressState state = ctx.getState();
int percentAbs = state.tickEnd() - state.tickStart();
int relativeValue = state.tickValue() - state.tickStart();
int percent = (relativeValue * 100) / percentAbs;
return String.format("%s%%", percent);
});
return cell;
};
private final static Function<Context, TextCell<Context>> DEFAULT_SPINNER_FACTORY =
item -> {
TextCell<Context> cell = TextCell.of(item, ctx -> {
Spinner spin = ctx.spinner();
int spinState = ctx.getState().sprinnerFrame();
return String.format("%s", spin.getFrames()[spinState]);
});
return cell;
};
/**
* Construct view with {@code tickStart 0} and {@code tickEnd 100}.
*/
public ProgressView() {
this(0, 100);
}
/**
* Construct view with given bounds for {@code tickStart} and {@code tickEnd}.
* {@code tickStart} needs to be equal or more than zero. {@code tickEnd} needs
* to be higher than {@code tickStart}. Defines default items for {@code text},
* {@code spinner} and {@code percent}.
*
* @param tickStart the tick start
* @param tickEnd the tick end
*/
public ProgressView(int tickStart, int tickEnd) {
this(tickStart, tickEnd, new ProgressViewItem[] { ProgressViewItem.ofText(), ProgressViewItem.ofSpinner(),
ProgressViewItem.ofPercent() });
}
/**
* Construct view with given bounds for {@code tickStart} and {@code tickEnd}.
* {@code tickStart} needs to be equal or more than zero. {@code tickEnd} needs
* to be higher than {@code tickStart}. Uses defined progress view items.
*
* @param tickStart the tick start
* @param tickEnd the tick end
* @param items the progress view items
*/
public ProgressView(int tickStart, int tickEnd, ProgressViewItem... items) {
Assert.isTrue(tickStart >= 0, "Start tick value must be greater or equal than zero");
Assert.isTrue(tickEnd > 0, "End tick value must be greater than zero");
Assert.isTrue(tickEnd > tickStart, "End tick value must be greater than start tick value");
this.tickStart = tickStart;
this.tickEnd = tickEnd;
this.tickValue = tickStart;
this.items = Arrays.asList(items);
initLayout();
}
/**
* Defines an item within a progress view. Allows to set item's factory, size
* and horizontal alignment.
*/
public static class ProgressViewItem {
private final Function<Context, TextCell<Context>> factory;
private final int size;
private final HorizontalAlign align;
public ProgressViewItem(Function<Context, TextCell<Context>> factory, int size, HorizontalAlign align) {
this.factory = factory;
this.size = size;
this.align = align;
}
public static ProgressViewItem ofText() {
return ofText(0, HorizontalAlign.CENTER);
}
public static ProgressViewItem ofText(int size, HorizontalAlign hAligh) {
return new ProgressViewItem(DEFAULT_DESCRIPTION_FACTORY, size, hAligh);
}
public static ProgressViewItem ofSpinner() {
return ofSpinner(0, HorizontalAlign.CENTER);
}
public static ProgressViewItem ofSpinner(int size, HorizontalAlign hAligh) {
return new ProgressViewItem(DEFAULT_SPINNER_FACTORY, size, hAligh);
}
public static ProgressViewItem ofPercent() {
return ofPercent(0, HorizontalAlign.CENTER);
}
public static ProgressViewItem ofPercent(int size, HorizontalAlign hAligh) {
return new ProgressViewItem(DEFAULT_PERCENT_FACTORY, size, hAligh);
}
}
/**
* Gets a progress description.
*
* @return a progress description
*/
public String getDescription() {
return description;
}
/**
* Sets a progress description. Used in items as a text item.
*
* @param description the progress description
*/
public void setDescription(String description) {
this.description = description;
}
public void start() {
if (running) {
return;
}
running = true;
ProgressState state = getState();
dispatch(ShellMessageBuilder.ofView(this, ProgressViewStartEvent.of(this, state)));
}
public void stop() {
if (!running) {
return;
}
running = false;
ProgressState state = getState();
dispatch(ShellMessageBuilder.ofView(this, ProgressViewEndEvent.of(this, state)));
}
private static class BoxWrapper extends BoxView {
TextCell<Context> delegate;
BoxWrapper(TextCell<Context> delegate) {
this.delegate = delegate;
}
@Override
protected void drawInternal(Screen screen) {
Rectangle rect = getRect();
delegate.setRect(rect.x(), rect.y(), rect.width(), rect.height());
delegate.draw(screen);
super.drawInternal(screen);
}
}
private void initLayout() {
grid = new GridView();
int[] columnSizes = new int[items.size()];
int index = 0;
for (ProgressViewItem item : items) {
columnSizes[index] = item.size;
TextCell<Context> cell = item.factory.apply(buildContext());
cell.setHorizontalAlign(item.align);
grid.addItem(new BoxWrapper(cell), 0, index, 1, 1, 0, 0);
index++;
}
grid.setRowSize(0);
grid.setColumnSize(columnSizes);
}
boolean needLastMillisInit = true;
private long lastMillis;
@Override
protected void drawInternal(Screen screen) {
long current = System.currentTimeMillis();
if (needLastMillisInit) {
needLastMillisInit = false;
lastMillis = current;
}
long elapsedFromLast = current - lastMillis;
if (elapsedFromLast > spinner.getInterval()) {
spinnerFrame = (spinnerFrame + 1) % spinner.getFrames().length;
lastMillis = current;
}
Rectangle rect = getRect();
int width = rect.width();
width = width / 3;
grid.setRect(rect.x(), rect.y(), rect.width(), rect.height());
grid.draw(screen);
super.drawInternal(screen);
}
/**
* Advance {@code tickValue} by a given count. Note that negative count
* will advance backwards.
*
* @param count the count to advance tick value
*/
public void tickAdvance(int count) {
setTickValue(tickValue + count);
}
/**
* Sets a tick value. If value is lower or higher than {@code tickStart} or
* {@code tickEnd} respectively {@code tickValue} will be set to low/high
* bounds. This means {@code tickValue} is always kept within range inclusively.
*
* @param value the new tick value to set
*/
public void setTickValue(int value) {
boolean changed = false;
if (value > tickEnd) {
changed = tickValue != tickEnd;
tickValue = tickEnd;
}
else if (value < tickStart) {
changed = tickValue != tickStart;
tickValue = tickStart;
}
else {
changed = tickValue != value;
tickValue = value;
}
if (changed) {
ProgressState state = getState();
dispatch(ShellMessageBuilder.ofView(this, ProgressViewStateChangeEvent.of(this, state)));
}
}
/**
* Gets a state of this {@code ProgressView}.
*
* @return a view progress state
*/
public ProgressState getState() {
return ProgressState.of(tickStart, tickEnd, tickValue, running, spinnerFrame);
}
private Context buildContext() {
return new Context() {
@Override
public String getDescription() {
return ProgressView.this.getDescription();
}
@Override
public ProgressState getState() {
return ProgressView.this.getState();
}
@Override
public ProgressView getView() {
return ProgressView.this;
}
@Override
public int resolveThemeStyle(String tag, int defaultStyle) {
return ProgressView.this.resolveThemeStyle(tag, defaultStyle);
}
@Override
public Spinner spinner() {
return ProgressView.this.spinner;
}
};
}
/**
* Context for {@code ProgressView} cell components.
*/
public interface Context {
/**
* Get a {@link ProgressView} description.
*
* @return a progress description
*/
String getDescription();
/**
* Get a state of a {@link ProgressView}.
*
* @return progress view state
*/
ProgressState getState();
/**
* Gets an encapsulating owner view.
*
* @return an owner view
*/
ProgressView getView();
/**
* Gets a {@link Spinner} frames.
*
* @return spinner frames
*/
Spinner spinner();
/**
* Resolve style using existing {@link ThemeResolver} and {@code theme name}.
* Use {@code defaultStyle} if resolving cannot happen.
*
* @param tag the style tag to use
* @param defaultStyle the default style to use
* @return resolved style
*/
int resolveThemeStyle(String tag, int defaultStyle);
}
/**
* Encapsulates a current running state of a {@link ProgressView}.
*
* @param tickStart the tick start value, zero or positive
* @param tickEnd the tick end value, positive and more than tick start
* @param tickValue the current tick value, within inclusive bounds of tick start/end
* @param running the running state
* @param spinnerFrame the current spinner frame index
*/
public record ProgressState(int tickStart, int tickEnd, int tickValue, boolean running, int sprinnerFrame) {
public static ProgressState of(int tickStart, int tickEnd, int tickValue, boolean running, int spinnerFrame) {
return new ProgressState(tickStart, tickEnd, tickValue, running, spinnerFrame);
}
}
/**
* {@link ViewEventArgs} for events using {@link ProgressState}.
*
* @param state the progress state
*/
public record ProgressViewStateEventArgs(ProgressState state) implements ViewEventArgs {
public static ProgressViewStateEventArgs of(ProgressState state) {
return new ProgressViewStateEventArgs(state);
}
}
/**
* {@link ViewEvent} indicating that proggress has been started.
*
* @param view the view sending an event
* @param args the event args
*/
public record ProgressViewStartEvent(View view, ProgressViewStateEventArgs args) implements ViewEvent {
public static ProgressViewStartEvent of(View view, ProgressState state) {
return new ProgressViewStartEvent(view, ProgressViewStateEventArgs.of(state));
}
}
/**
* {@link ViewEvent} indicating that proggress has been ended.
*
* @param view the view sending an event
* @param args the event args
*/
public record ProgressViewEndEvent(View view, ProgressViewStateEventArgs args) implements ViewEvent {
public static ProgressViewEndEvent of(View view, ProgressState state) {
return new ProgressViewEndEvent(view, ProgressViewStateEventArgs.of(state));
}
}
/**
* {@link ViewEvent} indicating that proggress state has been changed.
*
* @param view the view sending an event
* @param args the event args
*/
public record ProgressViewStateChangeEvent(View view, ProgressViewStateEventArgs args) implements ViewEvent {
public static ProgressViewStateChangeEvent of(View view, ProgressState state) {
return new ProgressViewStateChangeEvent(view, ProgressViewStateEventArgs.of(state));
}
}
}

View File

@@ -0,0 +1,342 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.view.control;
/**
* {@code Spinner} represents how user is notified that something is happening
* using a traditional spinner concept. Represented in a console with an array
* of characters which are looped.
*
* @author Janne Valkealahti
*/
public interface Spinner {
/**
* Gets a frame characters. Type is {@link String} to support unicode.
*
* @return a frame characters
*/
String[] getFrames();
/**
* Gets an interval which should be used to estimate how often frame
* should get changed. This is always an estimate as actual change
* depends how ofter console gets redrawn.
*
* @return an interval in milliseconds
*/
int getInterval();
/**
* Construct {@link Spinner} from given frames and interval.
*
* @param frames the spinner frames
* @param interval the spinner interval
* @return a Spinner implementation
*/
static Spinner of(String[] frames, int interval) {
return new Spinner() {
@Override
public String[] getFrames() {
return frames;
}
@Override
public int getInterval() {
return interval;
}
};
}
final static String[] LINE1 = new String[] {
"-",
"\\",
"|",
"/"
};
final static String[] DOTS1 = new String[] {
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS2 = new String[] {
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS3 = new String[] {
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS4 = new String[] {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS5 = new String[] {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS6 = new String[] {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS7 = new String[] {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS8 = new String[] {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS9 = new String[] {
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS10 = new String[] {
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS11 = new String[] {
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS12 = new String[] {
"⢀⠀",
"⡀⠀",
"⠄⠀",
"⢂⠀",
"⡂⠀",
"⠅⠀",
"⢃⠀",
"⡃⠀",
"⠍⠀",
"⢋⠀",
"⡋⠀",
"⠍⠁",
"⢋⠁",
"⡋⠁",
"⠍⠉",
"⠋⠉",
"⠋⠉",
"⠉⠙",
"⠉⠙",
"⠉⠩",
"⠈⢙",
"⠈⡙",
"⢈⠩",
"⡀⢙",
"⠄⡙",
"⢂⠩",
"⡂⢘",
"⠅⡘",
"⢃⠨",
"⡃⢐",
"⠍⡐",
"⢋⠠",
"⡋⢀",
"⠍⡁",
"⢋⠁",
"⡋⠁",
"⠍⠉",
"⠋⠉",
"⠋⠉",
"⠉⠙",
"⠉⠙",
"⠉⠩",
"⠈⢙",
"⠈⡙",
"⠈⠩",
"⠀⢙",
"⠀⡙",
"⠀⠩",
"⠀⢘",
"⠀⡘",
"⠀⠨",
"⠀⢐",
"⠀⡐",
"⠀⠠",
"⠀⢀",
"⠀⡀"
};
final static String[] DOTS13 = new String[] {
"",
"",
"",
"",
"",
"",
"",
""
};
final static String[] DOTS14 = new String[] {
". ",
" . ",
" . ",
" ."
};
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.view.control.cell;
import java.util.function.Function;
import org.springframework.shell.component.view.screen.Screen;
import org.springframework.shell.component.view.screen.Screen.Writer;
import org.springframework.shell.geom.HorizontalAlign;
import org.springframework.shell.geom.Rectangle;
import org.springframework.shell.geom.VerticalAlign;
import org.springframework.shell.style.StyleSettings;
/**
* Base implementation of a {@link TextCell}.
*
* @author Janne Valkealahti
*/
public abstract class AbstractTextCell<T> extends AbstractCell<T> implements TextCell<T> {
private Function<T, String> itemFunction;
private HorizontalAlign hAlign = HorizontalAlign.CENTER;
private VerticalAlign vAlign = VerticalAlign.CENTER;
public AbstractTextCell(T item, Function<T, String> itemFunction) {
this(item, itemFunction, HorizontalAlign.CENTER, VerticalAlign.CENTER);
}
public AbstractTextCell(T item, Function<T, String> itemFunction, HorizontalAlign hAlign, VerticalAlign vAlign) {
super(item);
this.itemFunction = itemFunction;
this.hAlign = hAlign;
this.vAlign = vAlign;
}
@Override
public void setHorizontalAlign(HorizontalAlign hAlign) {
this.hAlign = hAlign;
}
@Override
public void setVerticalAlign(VerticalAlign vAlign) {
this.vAlign = vAlign;
}
protected String getBackgroundStyle() {
return StyleSettings.TAG_BACKGROUND;
}
@Override
protected void drawBackground(Screen screen) {
Rectangle rect = getRect();
int bgColor = resolveThemeBackground(getBackgroundStyle(), getBackgroundColor(), -1);
if (bgColor > -1) {
screen.writerBuilder().build().background(rect, bgColor);
}
}
@Override
protected void drawContent(Screen screen) {
String text = itemFunction.apply(getItem());
if (text != null) {
Rectangle rect = getRect();
Writer writer = screen.writerBuilder().style(getStyle()).color(getForegroundColor()).build();
writer.text(text, rect, hAlign, vAlign);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.view.control.cell;
import java.util.function.Function;
import org.springframework.shell.geom.HorizontalAlign;
import org.springframework.shell.geom.VerticalAlign;
/**
* Extension of a {@link Cell} to make it aware of an item style and selection state.
*
* @author Janne Valkealahti
*/
public interface TextCell<T> extends Cell<T> {
/**
* Sets horizontal align for a text to draw. Defaults to
* {@link HorizontalAlign#CENTER}.
*
* @param hAlign the horizontal align
*/
void setHorizontalAlign(HorizontalAlign hAlign);
/**
* Sets vertical align for a text to draw. Defaults to
* {@link VerticalAlign#CENTER}.
*
* @param vAlign the vertical align
*/
void setVerticalAlign(VerticalAlign vAlign);
/**
* Helper method to build a {@code TextCell}.
*
* @param <T> type of an item
* @param item the item
* @param itemFunction the item function
* @return a default text cell
*/
static <T> TextCell<T> of(T item, Function<T, String> itemFunction) {
return new DefaultTextCell<T>(item, itemFunction);
}
/**
* Helper method to build a {@code TextCell}.
*
* @param <T> type of an item
* @param item the item
* @param itemFunction the item function
* @param hAlign item horizontal alignment
* @param vAlign item vertical alignment
* @return
*/
static <T> TextCell<T> of(T item, Function<T, String> itemFunction, HorizontalAlign hAlign, VerticalAlign vAlign) {
return new DefaultTextCell<T>(item, itemFunction, hAlign, vAlign);
}
static class DefaultTextCell<T> extends AbstractTextCell<T> {
DefaultTextCell(T item, Function<T, String> itemFunction) {
super(item, itemFunction);
}
DefaultTextCell(T item, Function<T, String> itemFunction, HorizontalAlign hAlign, VerticalAlign vAlign) {
super(item, itemFunction, hAlign, vAlign);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import org.springframework.test.util.ReflectionTestUtils;
public class AbstractViewTests {
protected Screen screen24x80;
protected Screen screen1x80;
protected Screen screen7x10;
protected Screen screen10x10;
protected Screen screen0x0;
@@ -44,6 +45,7 @@ public class AbstractViewTests {
@BeforeEach
void setup() {
screen24x80 = new DefaultScreen(24, 80);
screen1x80 = new DefaultScreen(1, 80);
screen7x10 = new DefaultScreen(7, 10);
screen0x0 = new DefaultScreen();
screen10x10 = new DefaultScreen(10, 10);
@@ -60,6 +62,7 @@ public class AbstractViewTests {
protected void clearScreens() {
screen24x80.resize(24, 80);
screen1x80.resize(1, 80);
screen7x10.resize(7, 10);
screen0x0.resize(0, 0);
screen10x10.resize(10, 10);

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.view.control;
import java.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.shell.component.view.control.ProgressView.ProgressViewEndEvent;
import org.springframework.shell.component.view.control.ProgressView.ProgressViewItem;
import org.springframework.shell.component.view.control.ProgressView.ProgressViewStartEvent;
import org.springframework.shell.component.view.control.ProgressView.ProgressViewStateChangeEvent;
import static org.assertj.core.api.Assertions.assertThat;
public class ProgressViewTests extends AbstractViewTests {
@Nested
class Construction {
ProgressView view;
@Test
void constructDefault() {
view = new ProgressView();
assertThat(view.getState().tickValue()).isEqualTo(0);
}
@Test
void constructBounds() {
view = new ProgressView(10, 30);
assertThat(view.getState().tickValue()).isEqualTo(10);
assertThat(view.getState().tickStart()).isEqualTo(10);
assertThat(view.getState().tickEnd()).isEqualTo(30);
}
void xxx() {
view = new ProgressView(10, 30, ProgressViewItem.ofText());
ProgressViewItem.ofText();
}
}
@Nested
class Visual {
ProgressView view;
@BeforeEach
void setup() {
view = new ProgressView();
view.setDescription("name");
view.setRect(0, 0, 80, 1);
configure(view);
}
@Test
void defaultItems() {
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("name", 11, 0, 4);
assertThat(forScreen(screen1x80)).hasHorizontalText("-", 39, 0, 1);
assertThat(forScreen(screen1x80)).hasHorizontalText("0%", 65, 0, 2);
}
@Test
void advance() {
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("0%", 65, 0, 2);
view.tickAdvance(5);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("5%", 65, 0, 2);
view.tickAdvance(5);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("10%", 65, 0, 3);
}
}
@Nested
class State {
ProgressView view;
@BeforeEach
void setup() {
view = new ProgressView();
view.setRect(0, 0, 20, 1);
configure(view);
}
@Test
void dontAdvanceBounds() {
assertThat(view.getState().tickValue()).isEqualTo(0);
view.tickAdvance(-1);
assertThat(view.getState().tickValue()).isEqualTo(0);
view.tickAdvance(100);
assertThat(view.getState().tickValue()).isEqualTo(100);
view.tickAdvance(1);
assertThat(view.getState().tickValue()).isEqualTo(100);
}
}
@Nested
class Events {
ProgressView view;
@BeforeEach
void setup() {
view = new ProgressView();
view.setRect(0, 0, 20, 1);
configure(view);
}
@Test
void plainStartStop() {
Flux<ProgressViewStartEvent> startEvents = eventLoop.viewEvents(ProgressViewStartEvent.class);
Flux<ProgressViewEndEvent> endEvents = eventLoop.viewEvents(ProgressViewEndEvent.class);
StepVerifier startVerifier = StepVerifier.create(startEvents)
.expectNextCount(1)
.thenCancel()
.verifyLater();
StepVerifier endVerifier = StepVerifier.create(endEvents)
.expectNextCount(1)
.thenCancel()
.verifyLater();
view.start();
view.stop();
startVerifier.verify(Duration.ofSeconds(1));
endVerifier.verify(Duration.ofSeconds(1));
}
@Test
void stateChangeWithTickValue() {
Flux<ProgressViewStateChangeEvent> changeEvents = eventLoop.viewEvents(ProgressViewStateChangeEvent.class);
StepVerifier verifier = StepVerifier.create(changeEvents)
.expectNextCount(1)
.thenCancel()
.verifyLater();
view.setTickValue(1);
verifier.verify(Duration.ofSeconds(1));
}
}
@Nested
class Styling {
@Test
void hasBorder() {
ProgressView view = new ProgressView();
configure(view);
view.setShowBorder(true);
view.setRect(0, 0, 80, 24);
view.draw(screen24x80);
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
}
}
}