diff --git a/packages/serverless-workflow-diagram-editor/src/core/workflowSdk.ts b/packages/serverless-workflow-diagram-editor/src/core/workflowSdk.ts index 3f0e25d..6605f50 100644 --- a/packages/serverless-workflow-diagram-editor/src/core/workflowSdk.ts +++ b/packages/serverless-workflow-diagram-editor/src/core/workflowSdk.ts @@ -45,7 +45,7 @@ export function parseWorkflow(text: string): WorkflowParseResult { } if (raw == null || typeof raw !== "object") { - return { model: null, errors: [new Error("Not a valid workflow object")] }; + return { model: null, errors: [new Error("Not a valid workflow")] }; } const model = new Classes.Workflow(raw) as Specification.Workflow; diff --git a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx index 19ad108..c442459 100644 --- a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx +++ b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx @@ -17,8 +17,10 @@ import * as React from "react"; import { Diagram, DiagramRef } from "../react-flow/diagram/Diagram"; import { DiagramEditorContextProvider } from "../store/DiagramEditorContextProvider"; -import { I18nProvider, useI18n, detectLocale } from "@serverlessworkflow/i18n"; +import { I18nProvider, detectLocale } from "@serverlessworkflow/i18n"; import { dictionaries } from "../i18n/locales"; +import { useDiagramEditorContext } from "../store/DiagramEditorContext"; +import { ParsingErrorPage } from "./error-pages/ParsingErrorPage"; /** * DiagramEditor component API */ @@ -33,19 +35,28 @@ export type DiagramEditorProps = { ref?: React.Ref; }; -const Content = () => { - const { t } = useI18n(); - return

{t("helloMessage")}

; +const DiagramEditorContent = ({ + diagramRef, + diagramDivRef, +}: { + diagramRef: React.RefObject; + diagramDivRef: React.RefObject; +}) => { + const { model } = useDiagramEditorContext(); + return model === null ? ( + + ) : ( + + ); }; export const DiagramEditor = (props: DiagramEditorProps) => { - // TODO: i18n - // TODO: store, context // TODO: ErrorBoundary / fallback // Refs const diagramDivRef = React.useRef(null); const diagramRef = React.useRef(null); + const locale = React.useMemo(() => { const supportedLocales = Object.keys(dictionaries); return props.locale ?? detectLocale(supportedLocales); @@ -64,10 +75,13 @@ export const DiagramEditor = (props: DiagramEditorProps) => { return ( <> - + - - + diff --git a/packages/serverless-workflow-diagram-editor/src/diagram-editor/error-pages/ErrorPage.tsx b/packages/serverless-workflow-diagram-editor/src/diagram-editor/error-pages/ErrorPage.tsx new file mode 100644 index 0000000..81d74ab --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/diagram-editor/error-pages/ErrorPage.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +type ErrorPageProps = { + title: string; + message?: string | undefined; + snippet?: string | undefined; +}; + +export const ErrorPage = ({ title, message, snippet }: ErrorPageProps) => { + // TODO: Apply styling later + return ( +
+

{title}

+ {message ?

{message}

: null} + {snippet ? ( +
+          {snippet}
+        
+ ) : null} +
+ ); +}; diff --git a/packages/serverless-workflow-diagram-editor/src/diagram-editor/error-pages/ParsingErrorPage.tsx b/packages/serverless-workflow-diagram-editor/src/diagram-editor/error-pages/ParsingErrorPage.tsx new file mode 100644 index 0000000..7b3cb46 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/diagram-editor/error-pages/ParsingErrorPage.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +import { useI18n } from "@serverlessworkflow/i18n"; +import { useDiagramEditorContext } from "../../store/DiagramEditorContext"; +import { ErrorPage } from "./ErrorPage"; + +type YAMLExceptionLike = Error & { + reason?: string; + mark?: { line: number; column: number; snippet?: string }; +}; + +const isYAMLException = (err: Error): err is YAMLExceptionLike => err.name === "YAMLException"; + +export const ParsingErrorPage = () => { + const { errors } = useDiagramEditorContext(); + const { t } = useI18n(); + // YAML parsing errors the only errors we expect for now so we will just take the first/only error + const err = errors[0]; + + if (err && isYAMLException(err)) { + return ( + + ); + } + + // Fallback (covers both no errors and non-YAML errors) + return ; +}; diff --git a/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts b/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts index 03b591f..41e6d4b 100644 --- a/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts +++ b/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts @@ -15,7 +15,9 @@ */ export const en = { - helloMessage: "Hello from Serverless Workflow Specification Editor!", + "workflowError.title": "Workflow Error", + "workflowError.default": "There was an error loading the workflow.", + "workflowError.parsing.title": "Parsing Error", } as const; export type TranslationKeys = keyof typeof en; diff --git a/packages/serverless-workflow-diagram-editor/src/i18n/locales/index.ts b/packages/serverless-workflow-diagram-editor/src/i18n/locales/index.ts index 4806f68..3ddf474 100644 --- a/packages/serverless-workflow-diagram-editor/src/i18n/locales/index.ts +++ b/packages/serverless-workflow-diagram-editor/src/i18n/locales/index.ts @@ -15,9 +15,7 @@ */ import { en } from "./en"; -import { fr } from "./fr"; export const dictionaries = { en, - fr, }; diff --git a/packages/serverless-workflow-diagram-editor/stories/DiagramEditor.stories.ts b/packages/serverless-workflow-diagram-editor/stories/DiagramEditor.stories.ts index 3642254..6a36333 100644 --- a/packages/serverless-workflow-diagram-editor/stories/DiagramEditor.stories.ts +++ b/packages/serverless-workflow-diagram-editor/stories/DiagramEditor.stories.ts @@ -17,6 +17,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { DiagramEditor } from "./DiagramEditor"; +import { BASIC_VALID_WORKFLOW_YAML } from "../tests/fixtures/workflows"; const meta = { title: "Example/DiagramEditor", @@ -37,6 +38,6 @@ export const Component: Story = { args: { isReadOnly: true, locale: "en", - content: "", // TODO: Replace with a sample workflow YAML once diagram renders from model + content: BASIC_VALID_WORKFLOW_YAML, // TODO: Add better workflow sample when removing hardcoded nodes and edges in Diagram component }, }; diff --git a/packages/serverless-workflow-diagram-editor/stories/ErrorPage.stories.ts b/packages/serverless-workflow-diagram-editor/stories/ErrorPage.stories.ts new file mode 100644 index 0000000..6af678f --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/stories/ErrorPage.stories.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ErrorPage } from "../src/diagram-editor/error-pages/ErrorPage"; + +const meta = { + title: "Example/ErrorPage", + component: ErrorPage, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ["autodocs"], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: "fullscreen", + }, + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const TitleOnly: Story = { + args: { + title: "Something went wrong", + }, +}; + +export const WithMessage: Story = { + args: { + title: "Something went wrong", + message: "An unexpected error occurred while processing your request.", + }, +}; + +export const WithSnippet: Story = { + args: { + title: "YAML Syntax Error", + snippet: `tasks: + - myTask + call: http + method: get, + endpoint: "http://example.com/api" + `, + }, +}; + +export const WithMessageAndSnippet: Story = { + args: { + title: "YAML Syntax Error", + message: "Bad indentation", + snippet: `tasks: + - myTask + call: http + method: get, + endpoint: "http://example.com/api" + `, + }, +}; diff --git a/packages/serverless-workflow-diagram-editor/tests/core/workflowSdk.integration.test.ts b/packages/serverless-workflow-diagram-editor/tests/core/workflowSdk.integration.test.ts index 24fe2ad..7be6b0c 100644 --- a/packages/serverless-workflow-diagram-editor/tests/core/workflowSdk.integration.test.ts +++ b/packages/serverless-workflow-diagram-editor/tests/core/workflowSdk.integration.test.ts @@ -52,7 +52,7 @@ describe("parseWorkflow", () => { ])("returns null model with error for $description", ({ input }) => { const result = parseWorkflow(input); expect(result.model).toBeNull(); - expect(result.errors[0].message).toBe("Not a valid workflow object"); + expect(result.errors[0].message).toBe("Not a valid workflow"); }); it("returns null model with errors for unparseable text", () => { diff --git a/packages/serverless-workflow-diagram-editor/tests/diagram-editor/DiagramEditor.story.test.tsx b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/DiagramEditor.story.test.tsx index 9fb4b77..0e4a141 100644 --- a/packages/serverless-workflow-diagram-editor/tests/diagram-editor/DiagramEditor.story.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/DiagramEditor.story.test.tsx @@ -17,7 +17,7 @@ import { render, screen } from "@testing-library/react"; import { composeStories } from "@storybook/react-vite"; import * as stories from "../../stories/DiagramEditor.stories"; -import { vi, test, expect, afterEach, describe } from "vitest"; +import { vi, expect, afterEach, describe, it } from "vitest"; import { BASIC_VALID_WORKFLOW_YAML } from "../fixtures/workflows"; // Composes all stories in the file @@ -28,7 +28,7 @@ describe("Story - DiagramEditor component", () => { vi.restoreAllMocks(); }); - test("Renders react flow Diagram component", async () => { + it("Renders react flow Diagram component", async () => { const locale = "en"; const isReadOnly = true; diff --git a/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ErrorPage.story.test.tsx b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ErrorPage.story.test.tsx new file mode 100644 index 0000000..381e866 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ErrorPage.story.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +import { render, screen } from "@testing-library/react"; +import { composeStories } from "@storybook/react-vite"; +import * as stories from "../../../stories/ErrorPage.stories"; +import { expect, describe, it } from "vitest"; + +const { TitleOnly, WithMessage, WithSnippet, WithMessageAndSnippet } = composeStories(stories); + +describe("Story - ErrorPage component", () => { + it("Renders title only", async () => { + render(); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + }); + + it("Renders with message", async () => { + render(); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + expect(screen.getByText("An unexpected error occurred while processing your request.")).toBeInTheDocument(); + }); + + it("Renders with Snippet", async () => { + render(); + expect(screen.getByText("YAML Syntax Error")).toBeInTheDocument(); + expect(screen.getByText(/call: http/)).toBeInTheDocument(); + }); + + it("Renders with message and snippet", async () => { + render(); + expect(screen.getByText("YAML Syntax Error")).toBeInTheDocument(); + expect(screen.getByText("Bad indentation")).toBeInTheDocument(); + expect(screen.getByText(/call: http/)).toBeInTheDocument(); + }); +}); diff --git a/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ErrorPage.test.tsx b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ErrorPage.test.tsx new file mode 100644 index 0000000..2eda1e1 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ErrorPage.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +import { render, screen } from "@testing-library/react"; +import { it, expect, describe } from "vitest"; +import { ErrorPage } from "../../../src/diagram-editor/error-pages/ErrorPage"; + +type ErrorTestCase = { + scenario: string; + props: { + title: string; + message?: string; + snippet?: string; + }; + expectMessage: boolean; + expectSnippet: boolean; +}; + +describe("ErrorPage", () => { + const testCases: ErrorTestCase[] = [ + { + scenario: "title only", + props: { title: "Something went wrong" }, + expectMessage: false, + expectSnippet: false, + }, + { + scenario: "title and message", + props: { title: "Error", message: "Please try again later." }, + expectMessage: true, + expectSnippet: false, + }, + { + scenario: "title, message, snippet", + props: { + title: "Parsing Error", + message: "Please try again later.", + snippet: "Error at line 3", + }, + expectMessage: true, + expectSnippet: true, + }, + { + scenario: "title and snippet without message", + props: { title: "Parsing Error", snippet: "Error at line 3" }, + expectMessage: false, + expectSnippet: true, + }, + ]; + + it.each(testCases)("Renders $scenario", ({ props, expectMessage, expectSnippet }) => { + render(); + + expect(screen.getByText(props.title)).toBeInTheDocument(); + + if (expectMessage) { + expect(screen.getByText(props.message!)).toBeInTheDocument(); + } + + if (expectSnippet) { + expect(screen.getByText(props.snippet!)).toBeInTheDocument(); + } + }); +}); diff --git a/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ParsingErrorPage.test.tsx b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ParsingErrorPage.test.tsx new file mode 100644 index 0000000..9821a5d --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/diagram-editor/error-pages/ParsingErrorPage.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +import { screen } from "@testing-library/react"; +import { it, expect, describe } from "vitest"; +import { ParsingErrorPage } from "../../../src/diagram-editor/error-pages/ParsingErrorPage"; +import { renderWithProviders, t } from "../../test-utils"; + +const createMockYAMLException = (reason?: string, snippet?: string): Error => { + const error = new Error("YAMLException") as Error & { + reason?: string; + mark?: { line: number; column: number; snippet?: string }; + }; + error.name = "YAMLException"; + error.reason = reason; + if (snippet !== undefined) { + error.mark = { line: 0, column: 0, snippet }; + } + return error; +}; + +const renderWithErrors = (errors: Error[]) => { + renderWithProviders(, { errors }); +}; + +describe("ParsingErrorPage", () => { + it.each([ + { scenario: "no errors", errors: [] }, + { scenario: "default error", errors: [new Error("Not a valid workflow")] }, + { scenario: "unknown error", errors: [new Error("Unknown error")] }, + ])("Falls back to default error message for $scenario", ({ errors }) => { + renderWithErrors(errors); + + expect(screen.getByText(t("workflowError.title"))).toBeInTheDocument(); + expect(screen.getByText(t("workflowError.default"))).toBeInTheDocument(); + }); + + it("Renders reason and snippet for YAMLException", () => { + renderWithErrors([createMockYAMLException("Unexpected token", "Error at line 3")]); + + expect(screen.getByText(t("workflowError.parsing.title"))).toBeInTheDocument(); + expect(screen.getByText("Unexpected token")).toBeInTheDocument(); + expect(screen.getByText("Error at line 3")).toBeInTheDocument(); + }); + + it("Renders reason without snippet if snippet is not provided in YAMLException", () => { + renderWithErrors([createMockYAMLException("Unexpected token")]); + + expect(screen.getByText(t("workflowError.parsing.title"))).toBeInTheDocument(); + expect(screen.getByText("Unexpected token")).toBeInTheDocument(); + }); + + it("Renders title only if reason is not provided in YAMLException", () => { + renderWithErrors([createMockYAMLException()]); + + expect(screen.getByText(t("workflowError.parsing.title"))).toBeInTheDocument(); + }); +}); diff --git a/packages/serverless-workflow-diagram-editor/src/i18n/locales/fr.ts b/packages/serverless-workflow-diagram-editor/tests/test-utils/index.ts similarity index 77% rename from packages/serverless-workflow-diagram-editor/src/i18n/locales/fr.ts rename to packages/serverless-workflow-diagram-editor/tests/test-utils/index.ts index 7a6e71b..26c207e 100644 --- a/packages/serverless-workflow-diagram-editor/src/i18n/locales/fr.ts +++ b/packages/serverless-workflow-diagram-editor/tests/test-utils/index.ts @@ -14,8 +14,5 @@ * limitations under the License. */ -import type { TranslationKeys } from "./en"; - -export const fr: Record = { - helloMessage: "Bonjour depuis l’éditeur de spécifications Serverless Workflow!", -}; +export { renderWithProviders } from "./render-helpers"; +export { t } from "./translation-helpers"; diff --git a/packages/serverless-workflow-diagram-editor/tests/test-utils/render-helpers.tsx b/packages/serverless-workflow-diagram-editor/tests/test-utils/render-helpers.tsx new file mode 100644 index 0000000..bbd3708 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/test-utils/render-helpers.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +import { render, type RenderOptions } from "@testing-library/react"; +import { I18nProvider } from "@serverlessworkflow/i18n"; +import { + DiagramEditorContext, + type DiagramEditorContextType, +} from "../../src/store/DiagramEditorContext"; +import { en } from "../../src/i18n/locales/en"; + +const noop = () => {}; + +/** + * Creates a mock DiagramEditorContext value with defaults. + * Allows partial overrides for specific test scenarios. + */ +export const createMockContextValue = ( + overrides?: Partial, +): DiagramEditorContextType => ({ + isReadOnly: true, + locale: "en", + model: null, + errors: [], + updateIsReadOnly: noop, + updateLocale: noop, + ...overrides, +}); + +/** + * Render function that wraps components with providers. + * Includes DiagramEditorContext and I18nProvider with default English translations. + * Example usage: + * renderWithProviders(, { + * errors: [new Error("Test error")], + * isReadOnly: false + * }); + */ +export const renderWithProviders = ( + ui: React.ReactElement, + contextValue?: Partial, + renderOptions?: Omit, +) => { + const mockContext = createMockContextValue(contextValue); + + return render( + + + {ui} + + , + renderOptions, + ); +}; diff --git a/packages/serverless-workflow-diagram-editor/tests/test-utils/translation-helpers.ts b/packages/serverless-workflow-diagram-editor/tests/test-utils/translation-helpers.ts new file mode 100644 index 0000000..391a3b5 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/test-utils/translation-helpers.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification 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 + * + * http://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. + */ + +import { en } from "../../src/i18n/locales/en"; + +/** + * Gets the translated text for a given translation key. + * Uses the actual translation dictionary to ensure tests stay in sync with translations. + * Example usage: + * expect(screen.getByText(t("workflowError.title"))).toBeInTheDocument(); + * Instead of: expect(screen.getByText("Workflow Error")).toBeInTheDocument(); + */ + +export const t = (key: keyof typeof en): string => { + return en[key]; +};