diff --git a/.github/workflows/build-js.yml b/.github/workflows/build-js.yml index a62ab9077..4d5c53955 100644 --- a/.github/workflows/build-js.yml +++ b/.github/workflows/build-js.yml @@ -15,10 +15,10 @@ jobs: fetch-depth: 0 - uses: pnpm/action-setup@v2 with: - version: 9.0.6 + version: 11.0.0 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm cache-dependency-path: "pnpm-lock.yaml" @@ -41,10 +41,10 @@ jobs: fetch-depth: 0 - uses: pnpm/action-setup@v2 with: - version: 9.0.6 + version: 11.0.0 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm cache-dependency-path: "pnpm-lock.yaml" @@ -53,3 +53,6 @@ jobs: - name: Build examples run: pnpm --filter "./examples/**" --workspace-concurrency=1 run build + + - name: Test vue-chat + run: cd examples/vue-chat && pnpm nuxt prepare && pnpm exec vitest run diff --git a/examples/vue-chat/README.md b/examples/vue-chat/README.md index 631604a69..15d250029 100644 --- a/examples/vue-chat/README.md +++ b/examples/vue-chat/README.md @@ -13,7 +13,7 @@ A chat application built with [Nuxt 3](https://nuxt.com), [Vercel AI SDK](https: ### Prerequisites -- Node.js 18+ +- Node.js 22+ - [pnpm](https://pnpm.io/) - An OpenAI API key @@ -48,11 +48,22 @@ The generated prompt lives at `generated/system-prompt.txt` and is checked in, s ### Run ```bash +pnpm --filter vue-chat prepare pnpm --filter vue-chat dev ``` Open [http://localhost:3000](http://localhost:3000). +### Test + +```bash +pnpm --filter vue-chat test +``` + +### Known issues + +- **Nuxt IPC socket error with `ssr: false`**: Nuxt 3.21.x has a bug where the dev server fails with `Vite Node IPC socket path not configured` in SPA mode. A workaround is included in `nuxt.config.ts` — see the comment there for details. This will be removed once the app upgrades to Nuxt 3.21.9+ where the bug is fixed upstream ([nuxt/nuxt#34957](https://github.com/nuxt/nuxt/issues/34957)). + ## Project structure ``` @@ -70,9 +81,10 @@ components/ └── openui/ # Vue component renderers for openui-lang output ├── Stack.vue ├── Card.vue - ├── TextContent.vue + ├── TextContent.vue # Renders text with markdown support ├── Button.vue - └── Chart.vue + ├── Chart.vue + └── __tests__/ # Component unit tests lib/ ├── library.ts # OpenUI component definitions (Stack, Card, TextContent, Button, Chart) └── tools.ts # AI tool definitions (weather, stocks, math, search) diff --git a/examples/vue-chat/components/openui/TextContent.vue b/examples/vue-chat/components/openui/TextContent.vue index 59a6f7a57..b1457dfae 100644 --- a/examples/vue-chat/components/openui/TextContent.vue +++ b/examples/vue-chat/components/openui/TextContent.vue @@ -1,7 +1,126 @@ + + diff --git a/examples/vue-chat/components/openui/__tests__/TextContent.test.ts b/examples/vue-chat/components/openui/__tests__/TextContent.test.ts new file mode 100644 index 000000000..ad5558072 --- /dev/null +++ b/examples/vue-chat/components/openui/__tests__/TextContent.test.ts @@ -0,0 +1,38 @@ +import { mount } from "@vue/test-utils"; +import { describe, expect, it } from "vitest"; +import { nextTick } from "vue"; +import TextContent from "../TextContent.vue"; + +const mountText = (text: string) => + mount(TextContent, { + props: { props: { text } }, + }); + +describe("TextContent", () => { + it("renders plain text", async () => { + const wrapper = mountText("Hello world"); + await nextTick(); + expect(wrapper.text()).toBe("Hello world"); + }); + + it("renders bold markdown", async () => { + const wrapper = mountText("**bold**"); + await nextTick(); + expect(wrapper.find("strong").exists()).toBe(true); + expect(wrapper.find("strong").text()).toBe("bold"); + }); + + it("renders inline code", async () => { + const wrapper = mountText("`code`"); + await nextTick(); + expect(wrapper.find("code").exists()).toBe(true); + expect(wrapper.find("code").text()).toBe("code"); + }); + + it("escapes raw HTML", async () => { + const wrapper = mountText(''); + await nextTick(); + expect(wrapper.html()).not.toContain("