use std::fmt::Display;
use std::time::Duration;
use leptos::attr::Attribute;
use leptos::html::Div;
use leptos::prelude::*;
use reactive_stores::Field;
use reactive_stores::Patch;
use reactive_stores::PatchField;
use reactive_stores::Store;
use reactive_stores::StoreFieldIterator;
use reactive_stores::StorePath;
use ui_components::widgets::details::DetailsParts;
use crate::Story;
use crate::story::Play;
use crate::story::Step;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[allow(dead_code)]
enum ExecutionMode {
Step,
Play,
#[default]
Stopped,
}
impl PatchField for ExecutionMode {
fn patch_field(&mut self, new: Self, path: &StorePath, notify: &mut dyn FnMut(&StorePath)) {
if *self != new {
*self = new;
notify(path);
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
enum TestResult {
Success,
Failure(&'static str),
InProgress,
#[default]
NotRun,
}
impl TestResult {
fn is_complete(&self) -> bool {
use TestResult::*;
match self {
Success | Failure(_) => true,
InProgress | NotRun => false,
}
}
}
impl PatchField for TestResult {
fn patch_field(&mut self, new: Self, path: &StorePath, notify: &mut dyn FnMut(&StorePath)) {
if *self != new {
*self = new;
notify(path);
}
}
}
impl Display for TestResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use TestResult::*;
match self {
Success => f.write_str("[SUCCESS]"),
Failure(reason) => {
f.write_str("[FAILURE: ")?;
f.write_str(reason)?;
f.write_str("]")
}
InProgress => f.write_str("[IN PROGRESS]"),
NotRun => f.write_str("[NOT RUN]"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Store, Patch)]
struct TestState {
test_id: usize,
result: TestResult,
}
#[derive(Debug, Store, Patch)]
struct TestViewModel {
#[store(key: usize = |state| state.test_id)]
step_results: Vec<TestState>,
next_step: usize,
mode: ExecutionMode,
result: TestResult,
}
impl TestViewModel {
fn new<P: Play>(play: &P) -> Self {
let steps_count: usize = play.steps().len();
let mut result: Vec<TestState> = Vec::with_capacity(steps_count);
for i in 0..steps_count {
result.push(TestState {
test_id: i,
result: TestResult::NotRun,
});
}
Self {
step_results: result,
next_step: 0,
mode: ExecutionMode::Stopped,
result: TestResult::NotRun,
}
}
}
pub struct TestView<S>
where
S: Story,
{
story: S,
play: usize,
state: Store<TestViewModel>,
canvas: NodeRef<Div>,
}
impl<S> TestView<S>
where
S: Story,
{
pub fn new(story: S, play: usize, canvas: NodeRef<Div>) -> Self {
let plays = story.plays();
let play_to_run = plays.get(play).unwrap();
let state: Store<TestViewModel> = Store::new(TestViewModel::new(play_to_run));
Self {
story,
play,
state,
canvas,
}
}
}
fn test_view_button_class<S: ToString>(extra_classes: S) -> impl Attribute {
let class = format!(
"leptos-forge-test-view-button bg-forgegray-300 -300 hover:bg-forgeblue-400 active:bg-forgeblue-600 active:text-forgegray-200 px-2 py-2 {}",
extra_classes.to_string()
);
view! {
<{..} class=class />
}
}
fn run_one_step<S: Story>(
state: Store<TestViewModel>,
story: &mut S,
steps: &[Box<dyn Step<Story = S>>],
canvas: NodeRef<Div>,
) {
let next_step: usize = state.next_step().get_untracked();
if next_step < steps.len() {
if TestResult::InProgress != state.result().get_untracked() {
state.result().patch(TestResult::InProgress);
}
state
.step_results()
.at_unkeyed(next_step)
.result()
.patch(TestResult::InProgress);
canvas.with(|canvas| {
if let Some(canvas) = canvas {
if let Err(e) = steps[next_step].run(canvas, story) {
let failure = TestResult::Failure(e);
state
.step_results()
.at_unkeyed(next_step)
.result()
.patch(failure);
state.result().patch(failure);
} else {
state
.step_results()
.at_unkeyed(next_step)
.result()
.patch(TestResult::Success);
state.next_step().patch(next_step + 1);
};
} else {
let failure = TestResult::Failure("Unable to get the reference to the canvas");
state
.step_results()
.at_unkeyed(next_step)
.result()
.patch(failure);
state.result().patch(failure);
}
});
}
let next_step: usize = state.next_step().get_untracked();
if next_step >= steps.len() {
let last_test_state = state
.step_results()
.at_unkeyed(next_step.saturating_sub(1))
.result()
.get_untracked();
state.result().patch(last_test_state);
}
}
fn play_steps<S: 'static + Story>(
delay: u64,
state: Store<TestViewModel>,
mut story: S,
play: usize,
canvas: NodeRef<Div>,
) {
let plays: Vec<Box<dyn Play<Story = S> + 'static>> = story.plays();
let play_to_run: &dyn Play<Story = S> = plays.get(play).unwrap();
let steps: Vec<Box<dyn Step<Story = S> + 'static>> = play_to_run.steps();
let next_step: usize = state.next_step().get_untracked();
let result: TestResult = state.result().get_untracked();
if next_step < steps.len() && !result.is_complete() {
run_one_step(state, &mut story, &steps, canvas);
let result: TestResult = state.result().get_untracked();
if !result.is_complete() {
set_timeout(
move || {
play_steps(delay, state, story, play, canvas);
},
Duration::from_millis(delay),
);
}
}
}
impl<S: 'static + Story> DetailsParts for TestView<S> {
fn summary(&self) -> AnyView {
let TestView {
mut story,
play,
state,
canvas,
} = *self;
let plays: Vec<Box<dyn Play<Story = S> + 'static>> = story.plays();
let play_to_run: &dyn Play<Story = S> = plays.get(play).unwrap();
let steps: Vec<Box<dyn Step<Story = S> + 'static>> = play_to_run.steps();
let play_test = move |_| {
play_steps(500, state, story, play, canvas);
};
let run_one_step = move |_| {
run_one_step(state, &mut story, &steps, canvas);
};
let result = move || format!("{}", state.result().get());
(view!{
<div class="leptos-forge-test-viewer flex flex-row bg-forgegray-100 items-center ">
<div class="leptos-forge-test-viewer-test-name flex-none text-base font-bold px-2 py-2">{result} - {play_to_run.description()}</div>
<div class="grow-1" inner_html=" "/>
<div class="leptos-forge-test-viewer-controls flex-none">
<button
on:click={play_test}
{..test_view_button_class("")}
>Play</button>
<button
on:click={run_one_step}
{..test_view_button_class("")}
>Step</button>
</div>
</div>
}).into_any()
}
fn details(&self) -> AnyView {
let play = self.play;
let story = self.story;
let store = self.state;
let plays = story.plays();
let play_to_run = plays.get(play).unwrap();
let steps = play_to_run
.steps()
.iter()
.map(|step| step.description())
.collect::<Vec<_>>();
(view! {
<ul class="list-none">
<For
each = move || store.step_results()
key = |result| result.test_id().get()
let:result
>
<StepView
description={
let idx = result.test_id().get();
if let Some(step) = steps.get(idx) {
step
}
else {
"🤯 Unknown test, please report an error"
}
}
state={result}
/>
</For>
</ul>
})
.into_any()
}
}
#[component]
fn StepView<S: ToString>(
description: S,
#[prop(into)]
state: Field<TestState>,
) -> impl IntoView {
let test_state = state.result();
let test_state = move || format!("{}", test_state.get());
let description = description.to_string();
view! {
<li>{test_state} - {description}</li>
}
}