Skip to content

Commit

Permalink
Merge pull request #193 from player-ui/sync/167e780a5a4576e4e0474dcce…
Browse files Browse the repository at this point in the history
…1548a053fa0fa87

Sync Up To Latest
  • Loading branch information
KetanReddy authored Oct 25, 2023
2 parents b44eb73 + dbf2921 commit d04ee7c
Show file tree
Hide file tree
Showing 52 changed files with 2,566 additions and 1,012 deletions.
3 changes: 2 additions & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
"code",
"doc",
"ideas",
"infra"
"infra",
"example"
]
},
{
Expand Down
3 changes: 2 additions & 1 deletion core/player/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ javascript_pipeline(
"@npm//arr-flatten",
"@npm//ebnf",
"@npm//timm",
"@npm//error-polyfill"
"@npm//error-polyfill",
"@npm//ts-nested-error"
],
test_data = [
"//core/make-flow:@player-ui/make-flow",
Expand Down
303 changes: 303 additions & 0 deletions core/player/src/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,79 @@ const flowWithItemsInArray: Flow = {
},
};

const multipleWarningsFlow: Flow = {
id: 'input-validation-flow',
views: [
{
type: 'view',
id: 'view',
loadWarning: {
asset: {
id: 'load-warning',
type: 'warning-asset',
binding: 'foo.load',
},
},
navigationWarning: {
asset: {
id: 'required-warning',
type: 'warning-asset',
binding: 'foo.navigation',
},
},
},
],
schema: {
ROOT: {
foo: {
type: 'FooType',
},
},
FooType: {
navigation: {
type: 'String',
validation: [
{
type: 'required',
severity: 'warning',
blocking: 'once',
trigger: 'navigation',
},
],
},
load: {
type: 'String',
validation: [
{
type: 'required',
severity: 'warning',
blocking: 'once',
trigger: 'load',
},
],
},
},
},
data: {},
navigation: {
BEGIN: 'FLOW_1',
FLOW_1: {
startState: 'VIEW_1',
VIEW_1: {
state_type: 'VIEW',
ref: 'view',
transitions: {
'*': 'END_Done',
},
},
END_Done: {
state_type: 'END',
outcome: 'done',
},
},
},
};

test('alt APIs', async () => {
const player = new Player();

Expand Down Expand Up @@ -1022,6 +1095,53 @@ describe('validation', () => {
const result = await flowResult;
expect(result.endState.outcome).toBe('test');
});

it('should auto-dismiss when dismissal is triggered', async () => {
player.start(multipleWarningsFlow);
const state = player.getState() as InProgressState;
const { flowResult } = state;
// Starts with one warning
expect(
state.controllers.view.currentView?.lastUpdate?.loadWarning.asset
.validation
).toBeDefined();

expect(
state.controllers.view.currentView?.lastUpdate?.navigationWarning.asset
.validation
).toBeUndefined();

// Try to transition
state.controllers.flow.transition('next');

// Stays on the same view
expect(
state.controllers.flow.current?.currentState?.value.state_type
).toBe('VIEW');

// new warning appears
expect(
state.controllers.view.currentView?.lastUpdate?.loadWarning.asset
.validation
).toBeDefined();

expect(
state.controllers.view.currentView?.lastUpdate?.navigationWarning.asset
.validation
).toBeDefined();

// Try to transition
state.controllers.flow.transition('next');

// Since data change (setting "sam") already triggered validation next step is auto dismiss
expect(
state.controllers.flow.current?.currentState?.value.state_type
).toBe('END');

// Should work now that there's no error
const result = await flowResult;
expect(result.endState.outcome).toBe('done');
});
});

describe('introspection and filtering', () => {
Expand Down Expand Up @@ -1270,6 +1390,62 @@ describe('errors', () => {
],
});

const oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow =
makeFlow({
id: 'view-1',
type: 'view',
thing1: {
asset: {
id: 'thing-1',
binding: 'foo.data.thing1',
type: 'input',
},
},
validation: [
{
type: 'required',
ref: 'foo.data.thing1',
severity: 'error',
trigger: 'load',
blocking: 'false',
},
{
type: 'required',
ref: 'foo.data.thing1',
trigger: 'navigation',
severity: 'warning',
},
],
});

const oneInputWithErrorOnLoadBlockingFalseAndWarningChangeTriggerFlow =
makeFlow({
id: 'view-1',
type: 'view',
thing1: {
asset: {
id: 'thing-1',
binding: 'foo.data.thing1',
type: 'input',
},
},
validation: [
{
type: 'required',
ref: 'foo.data.thing1',
severity: 'error',
trigger: 'load',
blocking: 'false',
},
{
type: 'required',
ref: 'foo.data.thing1',
trigger: 'change',
severity: 'warning',
},
],
});

it('blocks navigation by default', async () => {
const player = new Player({ plugins: [new TrackBindingPlugin()] });
player.start(errorFlow);
Expand Down Expand Up @@ -1320,6 +1496,124 @@ describe('errors', () => {
'END'
);
});

it('error on load blocking false then warning with change trigger on navigation attempt', async () => {
const player = new Player({ plugins: [new TrackBindingPlugin()] });
player.start(
oneInputWithErrorOnLoadBlockingFalseAndWarningChangeTriggerFlow
);
const state = player.getState() as InProgressState;

expect(
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
).toMatchObject({
message: 'A value is required',
severity: 'error',
displayTarget: 'field',
});

// Try to navigate, should prevent the navigation and display the warning
state.controllers.flow.transition('next');
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
'VIEW'
);

expect(
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
).toMatchObject({
message: 'A value is required',
severity: 'warning',
displayTarget: 'field',
});

// Navigate _again_ this should dismiss it
state.controllers.flow.transition('next');
// We make it to the next state

expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
'END'
);
});

it('error on load blocking false then warning on navigation attempt', async () => {
const player = new Player({ plugins: [new TrackBindingPlugin()] });
player.start(
oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow
);
const state = player.getState() as InProgressState;

expect(
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
).toMatchObject({
message: 'A value is required',
severity: 'error',
displayTarget: 'field',
});

// Try to navigate, should prevent the navigation and display the warning
state.controllers.flow.transition('next');
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
'VIEW'
);

expect(
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
).toMatchObject({
message: 'A value is required',
severity: 'warning',
displayTarget: 'field',
});

// Navigate _again_ this should dismiss it
state.controllers.flow.transition('next');
// We make it to the next state

expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
'END'
);
});

it('error on load blocking false then input active then warning on navigation attempt', async () => {
const player = new Player({ plugins: [new TrackBindingPlugin()] });
player.start(
oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow
);
const state = player.getState() as InProgressState;

expect(
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
).toMatchObject({
message: 'A value is required',
severity: 'error',
displayTarget: 'field',
});

// Type something to dismiss the error, should be empty to see the warning
state.controllers.data.set([['foo.data.thing1', '']]);

// Try to navigate, should prevent the navigation and display the warning
state.controllers.flow.transition('next');
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
'VIEW'
);

expect(
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
).toMatchObject({
message: 'A value is required',
severity: 'warning',
displayTarget: 'field',
});

// Navigate _again_ this should dismiss it
state.controllers.flow.transition('next');
// We make it to the next state

expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
'END'
);
});

it('blocking false allows navigation', async () => {
const player = new Player({ plugins: [new TrackBindingPlugin()] });
player.start(nonBlockingErrorFlow);
Expand Down Expand Up @@ -3009,15 +3303,24 @@ describe('Validation in subflow', () => {

player.start(flow);

/**
*
*/
const getControllers = () => {
const state = player.getState() as InProgressState;
return state.controllers;
};

/**
*
*/
const getValidationMessage = () => {
return getControllers().view.currentView?.lastUpdate?.validation;
};

/**
*
*/
const attemptTransition = () => {
getControllers().flow.transition('next');
};
Expand Down
10 changes: 10 additions & 0 deletions core/player/src/binding-grammar/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
VALID_AST_PARSER_TESTS,
INVALID_AST_PARSER_TESTS,
VALID_AST_PARSER_CUSTOM_TESTS,
} from './test-utils/ast-cases';
import type { ParserSuccessResult, ParserFailureResult } from '../ast';
import { parse as parseParsimmon } from '../parsimmon';
Expand Down Expand Up @@ -49,4 +50,13 @@ describe('custom', () => {
expect(result.status).toBe(false);
expect((result as ParserFailureResult).error.length > 0).toBe(true);
});

test.each(VALID_AST_PARSER_CUSTOM_TESTS)(
'Custom Unicode Valid: %s',
(binding, AST) => {
const result = parseCustom(binding);
expect(result.status).toBe(true);
expect((result as ParserSuccessResult).path).toStrictEqual(AST);
}
);
});
Loading

0 comments on commit d04ee7c

Please sign in to comment.