diff --git a/.changeset/cool-popups-stay-open.md b/.changeset/cool-popups-stay-open.md new file mode 100644 index 0000000..7a6a3b2 --- /dev/null +++ b/.changeset/cool-popups-stay-open.md @@ -0,0 +1,5 @@ +--- +"@inkcre/ui-web": minor +--- + +Add modeless InkPopup rendering through the backward-compatible `scrim` prop. diff --git a/AGENTS.md b/AGENTS.md index f269c48..6c6858c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,4 +19,6 @@ This repository owns the InKCre design system and UI libraries. - Package manager is PNPM - Use changeset to summarize your changes (run `pnpm changeset` after you made changes.) -- Run tests and build to verify your changes if possible. +- Follow the organization-wide [Verification and Test Policy](https://github.com/InKCre/.github/blob/main/TESTING.md). + This repository prefers type-check, lint, generated-contract checks, story builds, and real + consuming-page review; it currently admits no automated unit/component suite. diff --git a/package.json b/package.json index eeca1bc..b0c484f 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,13 @@ "tokens:update": "tsx scripts/prepare-token-update.ts apply", "type-check": "pnpm type-check:root && pnpm --filter @inkcre/ui-web type-check", "type-check:root": "tsc --noEmit", - "test": "pnpm --filter @inkcre/ui-web test", "story": "pnpm --filter @inkcre/ui-web story:build", "story:dev": "pnpm --filter @inkcre/ui-web story:dev", "check:skills": "pnpm --filter @inkcre/ui-web skill:generate:check && pnpm --filter @inkcre/ui-web skill:validate", "format": "oxfmt .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md && pnpm --filter @inkcre/ui-web format", "format:check": "oxfmt --check .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md && pnpm --filter @inkcre/ui-web format:check", "lint": "oxlint --deny-warnings scripts && pnpm --filter @inkcre/ui-web lint", - "check": "pnpm format:check && pnpm lint && pnpm check:generated && pnpm check:skills && pnpm check:stories && pnpm check:token-workflow && pnpm type-check && pnpm test && pnpm build && pnpm check:package && pnpm story", + "check": "pnpm format:check && pnpm lint && pnpm check:generated && pnpm check:skills && pnpm check:stories && pnpm check:token-workflow && pnpm type-check && pnpm build && pnpm check:package && pnpm story", "changeset": "changeset", "changeset:add": "changeset add", "changeset:version": "changeset version && pnpm build-package-metadata", diff --git a/packages/web/.oxlintrc.json b/packages/web/.oxlintrc.json index 43ef7c2..8ab2c86 100644 --- a/packages/web/.oxlintrc.json +++ b/packages/web/.oxlintrc.json @@ -1,6 +1,6 @@ { "$schema": "../../node_modules/oxlint/configuration_schema.json", - "plugins": ["eslint", "oxc", "typescript", "unicorn", "vue", "vitest"], + "plugins": ["eslint", "oxc", "typescript", "unicorn", "vue"], "options": { "denyWarnings": true, "reportUnusedDisableDirectives": "error" diff --git a/packages/web/package.json b/packages/web/package.json index 53c0d30..07a9ac6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -84,9 +84,9 @@ "build:skills": "pnpm run skill:generate:check", "clean": "rimraf dist", "preview": "vite preview", - "format": "oxfmt --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js vitest.config.ts", - "format:check": "oxfmt --check --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js vitest.config.ts", - "lint": "oxlint --config .oxlintrc.json src stories histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js vitest.config.ts", + "format": "oxfmt --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js", + "format:check": "oxfmt --check --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js", + "lint": "oxlint --config .oxlintrc.json src stories histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js", "lint:fix": "pnpm run lint --fix", "skill:generate": "tsx ../../scripts/build-agent-skills.ts", "skill:generate:check": "tsx ../../scripts/build-agent-skills.ts --check", @@ -94,11 +94,7 @@ "story:dev": "histoire dev", "story:build": "histoire build", "story:preview": "histoire preview", - "type-check": "vue-tsc --noEmit -p tsconfig.check.json", - "test": "vitest run", - "test:watch": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest --coverage" + "type-check": "vue-tsc --noEmit -p tsconfig.check.json" }, "dependencies": { "@vue/shared": "3.5.25" @@ -116,13 +112,9 @@ "@unocss/preset-icons": "^66.5.10", "@unocss/preset-wind3": "^66.5.10", "@vitejs/plugin-vue": "^6.0.3", - "@vitest/coverage-v8": "^4.0.15", - "@vitest/ui": "^4.0.15", - "@vue/test-utils": "^2.4.6", "@vueuse/core": "^14.1.0", "cpr": "^3.0.1", "dayjs": "^1.11.19", - "happy-dom": "^20.0.11", "histoire": "1.0.0-alpha.5", "rimraf": "^6.0.1", "rollup-plugin-visualizer": "^6.0.5", @@ -130,7 +122,6 @@ "unocss": "^66.5.10", "vite": "^7.2.7", "vite-plugin-dts": "^4.5.4", - "vitest": "^4.0.15", "vscode-json-languageservice": "^5.6.4", "vscode-languageserver-textdocument": "^1.0.12", "vue": "^3.4.0", diff --git a/packages/web/skills/ui-web/references/components/InkPopup.md b/packages/web/skills/ui-web/references/components/InkPopup.md index 4803c19..8c93486 100644 --- a/packages/web/skills/ui-web/references/components/InkPopup.md +++ b/packages/web/skills/ui-web/references/components/InkPopup.md @@ -25,11 +25,11 @@ ## Public API Facts - Import: `import { InkPopup } from "@inkcre/ui-web";` -- Props: `closeOnScrim`, `position` +- Props: `closeOnScrim`, `position`, `scrim` - Events: `scrim-click` - Slots: `default` - Public types: None -- Story variants: `Default Popup` +- Story variants: `Default Popup`, `Modeless Popup` ## API Caveats diff --git a/packages/web/src/components/inkAutoForm/inkAutoForm.test.ts b/packages/web/src/components/inkAutoForm/inkAutoForm.test.ts deleted file mode 100644 index 8fea224..0000000 --- a/packages/web/src/components/inkAutoForm/inkAutoForm.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { mount } from "@vue/test-utils"; -import InkAutoForm from "./inkAutoForm.vue"; -import { mapSchemaPropertyToComponent, type JSONSchemaProperty } from "./inkAutoForm"; - -describe("inkAutoForm", () => { - describe("Component Rendering", () => { - it("renders form with valid schema", () => { - const schema = { - type: "object" as const, - properties: { - name: { - type: "string" as const, - title: "Name", - }, - }, - }; - - const wrapper = mount(InkAutoForm, { - props: { - schema, - formData: {}, - }, - }); - - expect(wrapper.find(".ink-auto-form__error").exists()).toBe(false); - }); - - it("shows error for invalid schema", () => { - const invalidSchema: any = { - type: "string", - }; - - const wrapper = mount(InkAutoForm, { - props: { - schema: invalidSchema, - formData: {}, - }, - }); - - expect(wrapper.find(".ink-auto-form__error").exists()).toBe(true); - expect(wrapper.find(".ink-auto-form__error").text()).toContain("Invalid schema"); - }); - }); - - describe("Schema-to-Component Mapping", () => { - it("maps string type to inkInput", () => { - const property: JSONSchemaProperty = { - type: "string", - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkInput"); - }); - - it("maps string with long maxLength to inkTextarea", () => { - const property: JSONSchemaProperty = { - type: "string", - maxLength: 200, - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkTextarea"); - }); - - it("maps boolean type to inkSwitch", () => { - const property: JSONSchemaProperty = { - type: "boolean", - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkSwitch"); - }); - - it("maps string with enum to inkDropdown", () => { - const property: JSONSchemaProperty = { - type: "string", - enum: ["option1", "option2", "option3"], - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkDropdown"); - expect(result.props.options).toHaveLength(3); - expect(result.props.options[0]).toEqual({ label: "option1", value: "option1" }); - }); - - it("maps string with date format to inkPicker", () => { - const property: JSONSchemaProperty = { - type: "string", - format: "date", - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkPicker"); - expect(result.props.type).toBe("date"); - }); - - it("maps string with time format to inkPicker", () => { - const property: JSONSchemaProperty = { - type: "string", - format: "time", - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkPicker"); - expect(result.props.type).toBe("time"); - }); - - it("maps string with date-time format to inkPicker", () => { - const property: JSONSchemaProperty = { - type: "string", - format: "date-time", - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkPicker"); - expect(result.props.type).toBe("datetime"); - }); - - it("maps number type to inkInput", () => { - const property: JSONSchemaProperty = { - type: "number", - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkInput"); - }); - - it("maps integer type to inkInput", () => { - const property: JSONSchemaProperty = { - type: "integer", - }; - - const result = mapSchemaPropertyToComponent(property); - - expect(result.component).toBe("inkInput"); - }); - }); - - describe("Form Data Updates", () => { - it("emits update:formData when field value changes", async () => { - const schema = { - type: "object" as const, - properties: { - name: { - type: "string" as const, - title: "Name", - }, - }, - }; - - const wrapper = mount(InkAutoForm, { - props: { - schema, - formData: {}, - }, - }); - - // Find the input component and trigger update - const input = wrapper.findComponent({ name: "InkInput" }); - await input.vm.$emit("update:modelValue", "John Doe"); - - expect(wrapper.emitted("update:formData")).toBeTruthy(); - const emitted = wrapper.emitted("update:formData") as any[]; - expect(emitted[emitted.length - 1][0]).toEqual({ name: "John Doe" }); - }); - }); - - describe("Default Values", () => { - it("initializes form data with default values from schema", async () => { - const schema = { - type: "object" as const, - properties: { - username: { - type: "string" as const, - title: "Username", - default: "guest", - }, - notifications: { - type: "boolean" as const, - title: "Notifications", - default: true, - }, - }, - }; - - const wrapper = mount(InkAutoForm, { - props: { - schema, - formData: {}, - }, - }); - - await wrapper.vm.$nextTick(); - - const emitted = wrapper.emitted("update:formData") as any[]; - expect(emitted).toBeTruthy(); - expect(emitted[0][0]).toEqual({ - username: "guest", - notifications: true, - }); - }); - }); -}); diff --git a/packages/web/src/components/inkButton/inkButton.test.ts b/packages/web/src/components/inkButton/inkButton.test.ts deleted file mode 100644 index 361e463..0000000 --- a/packages/web/src/components/inkButton/inkButton.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { mount } from "@vue/test-utils"; -import InkButton from "./inkButton.vue"; - -describe("InkButton", () => { - it("renders button with text", () => { - const wrapper = mount(InkButton, { - props: { - text: "Click me", - type: "primary", - size: "md", - }, - }); - - expect(wrapper.text()).toContain("Click me"); - }); - - it("applies correct CSS classes for theme, type and size", () => { - const wrapper = mount(InkButton, { - props: { - text: "Test", - theme: "danger", - type: "square", - size: "sm", - }, - }); - - const button = wrapper.find("button"); - expect(button.classes()).toContain("ink-button--theme-danger"); - expect(button.classes()).toContain("ink-button--type-square"); - expect(button.classes()).toContain("ink-button--size-sm"); - }); - - it("emits click event when clicked", async () => { - const wrapper = mount(InkButton, { - props: { - text: "Click me", - }, - }); - - await wrapper.find("button").trigger("click"); - expect(wrapper.emitted()).toHaveProperty("click"); - }); - - it("does not emit click event when loading", async () => { - const wrapper = mount(InkButton, { - props: { - text: "Click me", - isLoading: true, - }, - }); - - await wrapper.find("button").trigger("click"); - expect(wrapper.emitted()).not.toHaveProperty("click"); - expect(wrapper.find("button").attributes()).toHaveProperty("disabled"); - }); - - it("renders icon when provided", () => { - const wrapper = mount(InkButton, { - props: { - text: "Settings", - icon: "i-mdi-cog", - }, - }); - - expect(wrapper.find(".ink-button__icon").exists()).toBe(true); - expect(wrapper.find(".ink-button__icon").classes()).toContain("i-mdi-cog"); - }); - - it("uses default values when props not provided", () => { - const wrapper = mount(InkButton); - const button = wrapper.find("button"); - - expect(button.classes()).toContain("ink-button--theme-subtle"); - expect(button.classes()).toContain("ink-button--size-md"); - expect(button.classes()).toContain("ink-button--type-default"); - }); -}); diff --git a/packages/web/src/components/inkDialog/inkDialog.test.ts b/packages/web/src/components/inkDialog/inkDialog.test.ts deleted file mode 100644 index bcc34b6..0000000 --- a/packages/web/src/components/inkDialog/inkDialog.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { mount } from "@vue/test-utils"; -import InkDialog from "./inkDialog.vue"; -import { INK_I18N_KEY } from "../../i18n"; - -const transitionStubs = { - Teleport: { - template: "
", - }, - Transition: { - template: "
", - }, -}; - -describe("InkDialog", () => { - it("renders its public content and default actions", () => { - const wrapper = mount(InkDialog, { - props: { - modelValue: true, - title: "Test Dialog", - subtitle: "Test Subtitle", - }, - global: { - stubs: transitionStubs, - }, - }); - - expect(wrapper.get(".ink-dialog__title").text()).toBe("Test Dialog"); - expect(wrapper.get(".ink-dialog__subtitle").text()).toBe("Test Subtitle"); - expect(wrapper.text()).toContain("Cancel"); - expect(wrapper.text()).toContain("Confirm"); - }); - - it("emits cancel and requests closing when the cancel action is clicked", async () => { - const wrapper = mount(InkDialog, { - props: { - modelValue: true, - }, - global: { - stubs: transitionStubs, - }, - }); - - const cancelButton = wrapper.findAll("button").find((button) => button.text() === "Cancel"); - expect(cancelButton).toBeDefined(); - - await cancelButton!.trigger("click"); - - expect(wrapper.emitted("cancel")).toHaveLength(1); - expect(wrapper.emitted("update:modelValue")).toEqual([[false]]); - }); - - it("emits confirm when the confirm action is clicked", async () => { - const wrapper = mount(InkDialog, { - props: { - modelValue: true, - }, - global: { - stubs: transitionStubs, - }, - }); - - const confirmButton = wrapper.findAll("button").find((button) => button.text() === "Confirm"); - expect(confirmButton).toBeDefined(); - - await confirmButton!.trigger("click"); - - expect(wrapper.emitted("confirm")).toHaveLength(1); - }); - - it("supports custom action visibility and text", () => { - const wrapper = mount(InkDialog, { - props: { - modelValue: true, - showCancel: false, - confirmText: "Continue", - }, - global: { - stubs: transitionStubs, - }, - }); - - expect(wrapper.text()).not.toContain("Cancel"); - expect(wrapper.text()).toContain("Continue"); - }); - - it("applies position without closing when scrim close is disabled", async () => { - const wrapper = mount(InkDialog, { - props: { - modelValue: true, - position: "left", - closeOnScrim: false, - }, - global: { - stubs: transitionStubs, - }, - }); - - expect(wrapper.get(".ink-popup").classes()).toContain("ink-popup--left"); - - await wrapper.get(".ink-popup-overlay").trigger("click"); - - expect(wrapper.find(".ink-dialog").exists()).toBe(true); - expect(wrapper.emitted("update:modelValue")).toBeUndefined(); - }); - - it("renders translated action labels when i18n is provided", () => { - const mockI18n = { - t: (key: string) => - ({ - "dialog.cancel": "Translated Cancel", - "dialog.confirm": "Translated Confirm", - })[key] ?? key, - locale: { value: "en" }, - }; - - const wrapper = mount(InkDialog, { - props: { - modelValue: true, - }, - global: { - stubs: transitionStubs, - provide: { - [INK_I18N_KEY as symbol]: mockI18n, - }, - }, - }); - - expect(wrapper.text()).toContain("Translated Cancel"); - expect(wrapper.text()).toContain("Translated Confirm"); - }); -}); diff --git a/packages/web/src/components/inkDropdown/inkDropdown.test.ts b/packages/web/src/components/inkDropdown/inkDropdown.test.ts deleted file mode 100644 index 809aff2..0000000 --- a/packages/web/src/components/inkDropdown/inkDropdown.test.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { mount } from "@vue/test-utils"; -import { nextTick } from "vue"; -import InkDropdown from "./inkDropdown.vue"; -import InkButton from "../inkButton/inkButton.vue"; -import type { DropdownOption } from "./inkDropdown"; - -describe("InkDropdown", () => { - const staticOptions: DropdownOption[] = [ - { label: "Option 1", value: "opt1" }, - { label: "Option 2", value: "opt2" }, - { label: "Option 3", value: "opt3" }, - ]; - - it("renders with static options", () => { - const wrapper = mount(InkDropdown, { - props: { - options: staticOptions, - modelValue: "", - }, - }); - - expect(wrapper.find(".ink-dropdown").exists()).toBe(true); - }); - - it("displays placeholder when no value is selected", () => { - const wrapper = mount(InkDropdown, { - props: { - options: staticOptions, - modelValue: "", - placeholder: "Select an option", - }, - }); - - expect(wrapper.find(".ink-dropdown__value").text()).toBe("Select an option"); - }); - - it("displays selected option label", () => { - const wrapper = mount(InkDropdown, { - props: { - options: staticOptions, - modelValue: "opt2", - }, - }); - - expect(wrapper.find(".ink-dropdown__value").text()).toBe("Option 2"); - }); - - it("emits update:modelValue when option is selected", async () => { - const wrapper = mount(InkDropdown, { - props: { - options: staticOptions, - modelValue: "", - editable: true, - }, - }); - - // Open dropdown - await wrapper.find(".ink-dropdown").trigger("click"); - await nextTick(); - - // Select an option - const options = wrapper.findAll(".ink-dropdown__option"); - await options[1].trigger("click"); - await nextTick(); - - expect(wrapper.emitted("update:modelValue")).toBeTruthy(); - expect(wrapper.emitted("update:modelValue")?.[0]).toEqual(["opt2"]); - }); - - it("loads lazy options immediately when modelValue is set", async () => { - const lazyOptions: DropdownOption[] = [ - { label: "Lazy 1", value: "lazy1" }, - { label: "Lazy 2", value: "lazy2" }, - { label: "Lazy 3", value: "lazy3" }, - ]; - - const refresher = vi.fn<() => Promise>(async () => { - return Promise.resolve(lazyOptions); - }); - - mount(InkDropdown, { - props: { - refresher, - modelValue: "lazy2", - }, - }); - - // Wait for mounted hook and async loading - await nextTick(); - await vi.waitFor(() => expect(refresher).toHaveBeenCalled()); - - // The refresher should be called immediately on mount - expect(refresher).toHaveBeenCalledTimes(1); - }); - - it("does not load lazy options immediately when modelValue is empty", async () => { - const refresher = vi.fn<() => Promise>(async () => { - return Promise.resolve([]); - }); - - mount(InkDropdown, { - props: { - refresher, - modelValue: "", - }, - }); - - await nextTick(); - - // Refresher should not be called for empty modelValue - expect(refresher).not.toHaveBeenCalled(); - }); - - it("does not load lazy options immediately when modelValue is null", async () => { - const refresher = vi.fn<() => Promise>(async () => { - return Promise.resolve([]); - }); - - mount(InkDropdown, { - props: { - refresher, - modelValue: null, - }, - }); - - await nextTick(); - - // Refresher should not be called for null modelValue - expect(refresher).not.toHaveBeenCalled(); - }); - - it("does not load lazy options when static options already exist", async () => { - const refresher = vi.fn<() => Promise>(async () => { - return Promise.resolve([]); - }); - - mount(InkDropdown, { - props: { - options: staticOptions, - refresher, - modelValue: "opt1", - }, - }); - - await nextTick(); - - // Refresher should not be called when options are already provided - expect(refresher).not.toHaveBeenCalled(); - }); - - it("displays refresh button when refresher is provided", () => { - const wrapper = mount(InkDropdown, { - props: { - refresher: async () => [], - }, - }); - - expect(wrapper.find(".ink-dropdown__refresh").exists()).toBe(true); - }); - - it("calls refresher when refresh button is clicked", async () => { - const refresher = vi.fn<() => Promise>(async () => []); - - const wrapper = mount(InkDropdown, { - props: { - refresher, - editable: true, - }, - }); - - await wrapper.find(".ink-dropdown__refresh").trigger("click"); - await nextTick(); - - expect(refresher).toHaveBeenCalled(); - }); - - describe("stepping", () => { - const options = [ - { label: "Opt 1", value: "v1" }, - { label: "Opt 2", value: "v2" }, - { label: "Opt 3", value: "v3" }, - ]; - - it("loops forward correctly", async () => { - const wrapper = mount(InkDropdown, { - props: { - modelValue: "v1", - options, - enableStepping: true, - }, - }); - - const nextBtn = wrapper.findAllComponents(InkButton).at(1); - await nextBtn?.trigger("click"); - expect(wrapper.emitted("update:modelValue")?.[0]).toEqual(["v2"]); - - await wrapper.setProps({ modelValue: "v3" }); - await nextBtn?.trigger("click"); - expect(wrapper.emitted("update:modelValue")?.[1]).toEqual(["v1"]); - }); - - it("loops backward correctly", async () => { - const wrapper = mount(InkDropdown, { - props: { - modelValue: "v1", - options, - enableStepping: true, - }, - }); - - const prevBtn = wrapper.findAllComponents(InkButton).at(0); - await prevBtn?.trigger("click"); - expect(wrapper.emitted("update:modelValue")?.[0]).toEqual(["v3"]); - - await wrapper.setProps({ modelValue: "v2" }); - await prevBtn?.trigger("click"); - expect(wrapper.emitted("update:modelValue")?.[1]).toEqual(["v1"]); - }); - - it("is disabled when editable is false", () => { - const wrapper = mount(InkDropdown, { - props: { - options, - enableStepping: true, - editable: false, - }, - }); - - const buttons = wrapper.findAllComponents(InkButton); - expect(buttons.at(0)?.find("button").element.disabled).toBe(true); - expect(buttons.at(1)?.find("button").element.disabled).toBe(true); - }); - - it("is disabled when options are empty", () => { - const wrapper = mount(InkDropdown, { - props: { - options: [], - enableStepping: true, - }, - }); - - const buttons = wrapper.findAllComponents(InkButton); - expect(buttons.at(0)?.find("button").element.disabled).toBe(true); - expect(buttons.at(1)?.find("button").element.disabled).toBe(true); - }); - }); - - describe("keyboard navigation", () => { - const options: DropdownOption[] = [ - { label: "Option 1", value: "opt1" }, - { label: "Option 2", value: "opt2" }, - { label: "Option 3", value: "opt3" }, - ]; - - it("navigates down with ArrowDown key", async () => { - const wrapper = mount(InkDropdown, { - props: { - options, - modelValue: "", - editable: true, - }, - }); - - // Open dropdown - await wrapper.find(".ink-dropdown").trigger("click"); - await nextTick(); - - const container = wrapper.find(".ink-dropdown-container"); - - // Press ArrowDown - await container.trigger("keydown", { key: "ArrowDown" }); - await nextTick(); - - // First option should be hovered (index 1 after initial 0) - const optionElements = wrapper.findAll(".ink-dropdown__option"); - expect(optionElements[1].classes()).toContain("ink-dropdown__option--hovered"); - }); - - it("navigates up with ArrowUp key", async () => { - const wrapper = mount(InkDropdown, { - props: { - options, - modelValue: "", - editable: true, - }, - }); - - // Open dropdown - await wrapper.find(".ink-dropdown").trigger("click"); - await nextTick(); - - const container = wrapper.find(".ink-dropdown-container"); - - // Press ArrowUp (should wrap to last option) - await container.trigger("keydown", { key: "ArrowUp" }); - await nextTick(); - - // Last option should be hovered - const optionElements = wrapper.findAll(".ink-dropdown__option"); - expect(optionElements[2].classes()).toContain("ink-dropdown__option--hovered"); - }); - - it("wraps around when navigating past last option", async () => { - const wrapper = mount(InkDropdown, { - props: { - options, - modelValue: "", - editable: true, - }, - }); - - // Open dropdown - await wrapper.find(".ink-dropdown").trigger("click"); - await nextTick(); - - const container = wrapper.find(".ink-dropdown-container"); - - // Press ArrowDown 3 times (0 -> 1 -> 2 -> 0) - await container.trigger("keydown", { key: "ArrowDown" }); - await container.trigger("keydown", { key: "ArrowDown" }); - await container.trigger("keydown", { key: "ArrowDown" }); - await nextTick(); - - // Should wrap back to first option - const optionElements = wrapper.findAll(".ink-dropdown__option"); - expect(optionElements[0].classes()).toContain("ink-dropdown__option--hovered"); - }); - - it("selects hovered option with Enter key", async () => { - const wrapper = mount(InkDropdown, { - props: { - options, - modelValue: "", - editable: true, - }, - }); - - // Open dropdown - await wrapper.find(".ink-dropdown").trigger("click"); - await nextTick(); - - const container = wrapper.find(".ink-dropdown-container"); - - // Navigate to second option - await container.trigger("keydown", { key: "ArrowDown" }); - await nextTick(); - - // Press Enter to select - await container.trigger("keydown", { key: "Enter" }); - await nextTick(); - - expect(wrapper.emitted("update:modelValue")).toBeTruthy(); - expect(wrapper.emitted("update:modelValue")?.[0]).toEqual(["opt2"]); - }); - - it("closes dropdown with Escape key", async () => { - const wrapper = mount(InkDropdown, { - props: { - options, - modelValue: "", - editable: true, - }, - }); - - // Open dropdown - await wrapper.find(".ink-dropdown").trigger("click"); - await nextTick(); - - expect(wrapper.find(".ink-dropdown__options").exists()).toBe(true); - - const container = wrapper.find(".ink-dropdown-container"); - - // Press Escape - await container.trigger("keydown", { key: "Escape" }); - await nextTick(); - - expect(wrapper.find(".ink-dropdown__options").exists()).toBe(false); - }); - - it("initializes hover to current selection when opening", async () => { - const wrapper = mount(InkDropdown, { - props: { - options, - modelValue: "opt2", - editable: true, - }, - }); - - // Open dropdown - await wrapper.find(".ink-dropdown").trigger("click"); - await nextTick(); - - // Second option should be hovered (matching modelValue) - const optionElements = wrapper.findAll(".ink-dropdown__option"); - expect(optionElements[1].classes()).toContain("ink-dropdown__option--hovered"); - }); - }); -}); diff --git a/packages/web/src/components/inkField/inkField.test.ts b/packages/web/src/components/inkField/inkField.test.ts deleted file mode 100644 index 56ea984..0000000 --- a/packages/web/src/components/inkField/inkField.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { mount } from "@vue/test-utils"; -import InkField from "./inkField.vue"; - -describe("InkField", () => { - it("renders label and value", () => { - const wrapper = mount(InkField, { - props: { - label: "Username", - value: "john.doe", - layout: "col", - }, - }); - - expect(wrapper.text()).toContain("Username"); - expect(wrapper.text()).toContain("john.doe"); - }); - - it("applies correct layout class", () => { - const wrapper = mount(InkField, { - props: { - label: "Test", - layout: "inline", - }, - }); - - expect(wrapper.classes()).toContain("ink-field--inline"); - }); - - it("shows required indicator when required is true", () => { - const wrapper = mount(InkField, { - props: { - label: "Password", - required: true, - }, - }); - - expect(wrapper.find(".ink-field__required").exists()).toBe(true); - }); - - it("emits value-click when editable value is clicked", async () => { - const wrapper = mount(InkField, { - props: { - label: "Bio", - value: "Click me", - editable: true, - }, - }); - - await wrapper.find(".ink-field__value").trigger("click"); - expect(wrapper.emitted()).toHaveProperty("value-click"); - }); - - it("renders slot content when provided", () => { - const wrapper = mount(InkField, { - props: { - label: "Custom", - }, - slots: { - default: "", - }, - }); - - expect(wrapper.html()).toContain("Custom Input"); - }); -}); diff --git a/packages/web/src/components/inkImage/inkImage.test.ts b/packages/web/src/components/inkImage/inkImage.test.ts deleted file mode 100644 index 42dc239..0000000 --- a/packages/web/src/components/inkImage/inkImage.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { mount, type VueWrapper } from "@vue/test-utils"; -import InkImage from "./inkImage.vue"; - -const defaultProps = { - src: "https://example.com/image.jpg", - alt: "Test image", -}; - -const mountedWrappers: VueWrapper[] = []; - -const mountImage = (options: Parameters>[1] = {}) => { - const wrapper = mount(InkImage, { - ...options, - props: { - ...defaultProps, - ...options.props, - }, - global: { - ...options.global, - stubs: { - Teleport: { - template: "
", - }, - Transition: { - template: "
", - }, - ...options.global?.stubs, - }, - }, - }); - - mountedWrappers.push(wrapper); - return wrapper; -}; - -const openImage = async (wrapper: VueWrapper) => { - await wrapper.get("[data-testid='ink-image-thumbnail']").trigger("click"); -}; - -afterEach(() => { - mountedWrappers.splice(0).forEach((wrapper) => wrapper.unmount()); -}); - -describe("InkImage", () => { - describe("rendering", () => { - it("renders the thumbnail with accessible image attributes", () => { - const wrapper = mountImage(); - const image = wrapper.get("[data-testid='ink-image-img']"); - - expect(image.attributes("src")).toBe(defaultProps.src); - expect(image.attributes("alt")).toBe(defaultProps.alt); - expect(image.attributes("loading")).toBe("lazy"); - }); - - it("supports a custom thumbnail", () => { - const wrapper = mountImage({ - slots: { - thumbnail: "
Custom
", - }, - }); - - expect(wrapper.get(".custom-thumbnail").text()).toBe("Custom"); - expect(wrapper.find("[data-testid='ink-image-img']").exists()).toBe(false); - }); - - it("shows the expanded title and custom header", async () => { - const wrapper = mountImage({ - props: { - title: "Test Image Title", - }, - slots: { - "expanded-header": "
Header
", - }, - }); - - await openImage(wrapper); - - expect(wrapper.get("[data-testid='ink-scrim']").exists()).toBe(true); - expect(wrapper.get(".custom-header").text()).toBe("Header"); - expect(wrapper.get(".ink-image__expanded-title").text()).toBe("Test Image Title"); - }); - }); - - describe("expanded state", () => { - it("opens from the thumbnail and publishes the state change", async () => { - const wrapper = mountImage(); - - expect(wrapper.find("[data-testid='ink-scrim']").exists()).toBe(false); - - await openImage(wrapper); - - expect(wrapper.get("[data-testid='ink-scrim']").exists()).toBe(true); - expect(wrapper.emitted("expand")).toHaveLength(1); - expect(wrapper.emitted("update:expanded")).toEqual([[true]]); - }); - - it("closes from the scrim and publishes the close action", async () => { - const wrapper = mountImage(); - await openImage(wrapper); - - await wrapper.get("[data-testid='ink-scrim']").trigger("click"); - - expect(wrapper.find("[data-testid='ink-scrim']").exists()).toBe(false); - expect(wrapper.emitted("close")).toHaveLength(1); - expect(wrapper.emitted("update:expanded")).toEqual([[true], [false]]); - }); - - it("closes on Escape", async () => { - const wrapper = mountImage(); - await openImage(wrapper); - - document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); - await wrapper.vm.$nextTick(); - - expect(wrapper.find("[data-testid='ink-scrim']").exists()).toBe(false); - expect(wrapper.emitted("close")).toHaveLength(1); - expect(wrapper.emitted("update:expanded")).toEqual([[true], [false]]); - }); - - it("leaves a controlled model unchanged until its parent updates it", async () => { - const wrapper = mountImage({ - props: { - expanded: false, - }, - }); - - await openImage(wrapper); - - expect(wrapper.emitted("update:expanded")).toEqual([[true]]); - expect(wrapper.find("[data-testid='ink-scrim']").exists()).toBe(false); - - await wrapper.setProps({ expanded: true }); - expect(wrapper.get("[data-testid='ink-scrim']").exists()).toBe(true); - - await wrapper.get("[data-testid='ink-scrim']").trigger("click"); - expect(wrapper.emitted("update:expanded")).toEqual([[true], [false]]); - expect(wrapper.get("[data-testid='ink-scrim']").exists()).toBe(true); - - await wrapper.setProps({ expanded: false }); - expect(wrapper.find("[data-testid='ink-scrim']").exists()).toBe(false); - }); - }); - - it("emits the failed source with image load errors", async () => { - const wrapper = mountImage(); - - await wrapper.get("[data-testid='ink-image-img']").trigger("error"); - - expect(wrapper.emitted("error")).toHaveLength(1); - expect(wrapper.emitted("error")?.[0]?.[0]).toMatchObject({ - src: defaultProps.src, - }); - }); -}); diff --git a/packages/web/src/components/inkJsonEditor/inkJsonEditor.test.ts b/packages/web/src/components/inkJsonEditor/inkJsonEditor.test.ts deleted file mode 100644 index 6b23ce9..0000000 --- a/packages/web/src/components/inkJsonEditor/inkJsonEditor.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { mount } from "@vue/test-utils"; -import { nextTick } from "vue"; -import InkJsonEditor from "./inkJsonEditor.vue"; -import { jsonService } from "./jsonSchemaService"; - -// Mock the jsonService.configure method -vi.mock("./jsonSchemaService", () => ({ - jsonService: { - configure: vi.fn<(settings: unknown) => void>(), - parseJSONDocument: vi.fn<() => Record>(() => ({})), - doValidation: vi.fn<() => Promise>(async () => []), - doComplete: vi.fn<() => Promise>(async () => null), - }, -})); - -describe("InkJsonEditor", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("configures schema on mount when schema prop is provided", async () => { - const schema = { - type: "object", - properties: { - name: { type: "string" }, - }, - }; - - mount(InkJsonEditor, { - props: { - modelValue: "{}", - schema, - schemaUri: "test://schema.json", - }, - }); - - await nextTick(); - - expect(jsonService.configure).toHaveBeenCalledWith({ - schemas: [ - { - uri: "test://schema.json", - fileMatch: ["*"], - schema, - }, - ], - }); - }); - - it("reconfigures schema when schema prop changes", async () => { - const initialSchema = { - type: "object", - properties: { - name: { type: "string" }, - }, - }; - - const updatedSchema = { - type: "object", - properties: { - name: { type: "string" }, - version: { type: "string" }, - }, - }; - - const wrapper = mount(InkJsonEditor, { - props: { - modelValue: "{}", - schema: initialSchema, - schemaUri: "test://schema.json", - }, - }); - - await nextTick(); - const initialCallCount = (jsonService.configure as any).mock.calls.length; - - await wrapper.setProps({ schema: updatedSchema }); - await nextTick(); - - const finalCallCount = (jsonService.configure as any).mock.calls.length; - expect(finalCallCount).toBeGreaterThan(initialCallCount); - - const lastCall = (jsonService.configure as any).mock.calls[finalCallCount - 1]; - expect(lastCall[0]).toEqual({ - schemas: [ - { - uri: "test://schema.json", - fileMatch: ["*"], - schema: updatedSchema, - }, - ], - }); - }); - - it("does not configure schema when schema prop is not provided", async () => { - mount(InkJsonEditor, { - props: { - modelValue: "{}", - }, - }); - - await nextTick(); - - expect(jsonService.configure).toHaveBeenCalledWith({ - schemas: [], - }); - }); - - it("clears schema configuration when schema prop becomes undefined", async () => { - const initialSchema = { - type: "object", - properties: { - name: { type: "string" }, - }, - }; - - const wrapper = mount(InkJsonEditor, { - props: { - modelValue: "{}", - schema: initialSchema, - schemaUri: "test://schema.json", - }, - }); - - await nextTick(); - vi.clearAllMocks(); - - await wrapper.setProps({ schema: undefined }); - await nextTick(); - - expect(jsonService.configure).toHaveBeenCalledWith({ - schemas: [], - }); - }); -}); diff --git a/packages/web/src/components/inkLoading/inkLoading.test.ts b/packages/web/src/components/inkLoading/inkLoading.test.ts deleted file mode 100644 index 07f72bc..0000000 --- a/packages/web/src/components/inkLoading/inkLoading.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { mount } from "@vue/test-utils"; -import InkLoading from "./inkLoading.vue"; - -describe("InkLoading", () => { - it("renders loading spinner", () => { - const wrapper = mount(InkLoading); - expect(wrapper.find(".ink-loading").exists()).toBe(true); - }); - - it("has the correct structure with blocks", () => { - const wrapper = mount(InkLoading); - const blocks = wrapper.findAll(".ink-loading__block"); - expect(blocks).toHaveLength(3); - }); -}); diff --git a/packages/web/src/components/inkPagination/inkPagination.test.ts b/packages/web/src/components/inkPagination/inkPagination.test.ts deleted file mode 100644 index 901cd2e..0000000 --- a/packages/web/src/components/inkPagination/inkPagination.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { mount } from "@vue/test-utils"; -import InkPagination from "./inkPagination.vue"; - -describe("InkPagination", () => { - it("renders page buttons correctly for few pages", () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 1, - totalPages: 5, - }, - }); - - const pageButtons = wrapper.findAll(".ink-pagination__page"); - expect(pageButtons.length).toBe(5); - }); - - it("highlights the current page", () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 3, - totalPages: 5, - }, - }); - - const activeButton = wrapper.find(".ink-pagination__page--active"); - expect(activeButton.text()).toBe("3"); - }); - - it("emits page-change event when page button is clicked", async () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 1, - totalPages: 5, - }, - }); - - const pageButtons = wrapper.findAll(".ink-pagination__page"); - await pageButtons[2].trigger("click"); // Click page 3 - - expect(wrapper.emitted("page-change")).toBeTruthy(); - expect(wrapper.emitted("page-change")?.[0]).toEqual([3]); - }); - - it("disables prev button on first page", () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 1, - totalPages: 5, - }, - }); - - const prevButton = wrapper.find(".ink-pagination__nav--prev"); - expect(prevButton.attributes("disabled")).toBeDefined(); - }); - - it("disables next button on last page", () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 5, - totalPages: 5, - }, - }); - - const nextButton = wrapper.find(".ink-pagination__nav--next"); - expect(nextButton.attributes("disabled")).toBeDefined(); - }); - - it("emits page-change when prev button is clicked", async () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 3, - totalPages: 5, - }, - }); - - const prevButton = wrapper.find(".ink-pagination__nav--prev"); - await prevButton.trigger("click"); - - expect(wrapper.emitted("page-change")?.[0]).toEqual([2]); - }); - - it("emits page-change when next button is clicked", async () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 3, - totalPages: 5, - }, - }); - - const nextButton = wrapper.find(".ink-pagination__nav--next"); - await nextButton.trigger("click"); - - expect(wrapper.emitted("page-change")?.[0]).toEqual([4]); - }); - - it("shows ellipsis for many pages", () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 10, - totalPages: 20, - }, - }); - - const ellipsisButtons = wrapper.findAll(".ink-pagination__page--ellipsis"); - expect(ellipsisButtons.length).toBeGreaterThan(0); - }); - - it("does not emit event when clicking current page", async () => { - const wrapper = mount(InkPagination, { - props: { - currentPage: 3, - totalPages: 5, - }, - }); - - const activeButton = wrapper.find(".ink-pagination__page--active"); - await activeButton.trigger("click"); - - expect(wrapper.emitted("page-change")).toBeFalsy(); - }); - - describe("Text type", () => { - it("renders text buttons instead of icon buttons", () => { - const wrapper = mount(InkPagination, { - props: { - type: "text", - currentPage: 1, - totalPages: 5, - }, - }); - - const textNavButtons = wrapper.findAll(".ink-pagination__text-nav"); - expect(textNavButtons.length).toBe(2); - - const iconButtons = wrapper.findAll(".ink-pagination__nav"); - expect(iconButtons.length).toBe(0); - }); - - it("displays page info in 'x of y' format", () => { - const wrapper = mount(InkPagination, { - props: { - type: "text", - currentPage: 3, - totalPages: 10, - }, - }); - - const pageInfo = wrapper.find(".ink-pagination__page-info"); - expect(pageInfo.text()).toBe("3 of 10"); - }); - - it("does not render numbered page buttons", () => { - const wrapper = mount(InkPagination, { - props: { - type: "text", - currentPage: 1, - totalPages: 5, - }, - }); - - const pageButtons = wrapper.findAll(".ink-pagination__page"); - expect(pageButtons.length).toBe(0); - }); - - it("disables Previous button on first page", () => { - const wrapper = mount(InkPagination, { - props: { - type: "text", - currentPage: 1, - totalPages: 5, - }, - }); - - const prevButton = wrapper.findAll(".ink-pagination__text-nav")[0]; - expect(prevButton.attributes("disabled")).toBeDefined(); - }); - - it("disables Next button on last page", () => { - const wrapper = mount(InkPagination, { - props: { - type: "text", - currentPage: 5, - totalPages: 5, - }, - }); - - const nextButton = wrapper.findAll(".ink-pagination__text-nav")[1]; - expect(nextButton.attributes("disabled")).toBeDefined(); - }); - - it("emits page-change when Previous button is clicked", async () => { - const wrapper = mount(InkPagination, { - props: { - type: "text", - currentPage: 3, - totalPages: 5, - }, - }); - - const prevButton = wrapper.findAll(".ink-pagination__text-nav")[0]; - await prevButton.trigger("click"); - - expect(wrapper.emitted("page-change")?.[0]).toEqual([2]); - }); - - it("emits page-change when Next button is clicked", async () => { - const wrapper = mount(InkPagination, { - props: { - type: "text", - currentPage: 3, - totalPages: 5, - }, - }); - - const nextButton = wrapper.findAll(".ink-pagination__text-nav")[1]; - await nextButton.trigger("click"); - - expect(wrapper.emitted("page-change")?.[0]).toEqual([4]); - }); - }); -}); diff --git a/packages/web/src/components/inkPlaceholder/inkPlaceholder.test.ts b/packages/web/src/components/inkPlaceholder/inkPlaceholder.test.ts deleted file mode 100644 index 1fd5b4d..0000000 --- a/packages/web/src/components/inkPlaceholder/inkPlaceholder.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { mount } from "@vue/test-utils"; -import InkPlaceholder from "./inkPlaceholder.vue"; -import { INK_I18N_KEY } from "../../i18n"; - -describe("InkPlaceholder", () => { - it("renders with default empty state", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - }); - - expect(wrapper.find(".ink-placeholder").exists()).toBe(true); - expect(wrapper.find(".ink-placeholder--empty").exists()).toBe(true); - }); - - it("renders with error state", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "error", - }, - }); - - expect(wrapper.find(".ink-placeholder--error").exists()).toBe(true); - }); - - it("displays custom title", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - title: "Custom Title", - }, - }); - - expect(wrapper.text()).toContain("Custom Title"); - }); - - it("displays custom description", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - description: "Custom Description", - }, - }); - - expect(wrapper.text()).toContain("Custom Description"); - }); - - it("displays default title for empty state", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - }); - - expect(wrapper.text()).toContain("No data"); - }); - - it("displays default title for error state", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "error", - }, - }); - - expect(wrapper.text()).toContain("Something went wrong"); - }); - - it("renders actions slot when provided", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - slots: { - actions: '', - }, - }); - - expect(wrapper.find(".ink-placeholder__actions").exists()).toBe(true); - expect(wrapper.find(".test-action").exists()).toBe(true); - }); - - it("does not render actions container when no actions slot provided", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - }); - - expect(wrapper.find(".ink-placeholder__actions").exists()).toBe(false); - }); - - it("renders custom illustration slot", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - slots: { - illustration: '
🎨
', - }, - }); - - expect(wrapper.find(".custom-icon").exists()).toBe(true); - }); - - it("renders custom title slot", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - slots: { - title: '

Custom

', - }, - }); - - expect(wrapper.find(".custom-title").exists()).toBe(true); - }); - - it("renders custom description slot", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - slots: { - description: '

Custom description

', - }, - }); - - expect(wrapper.find(".custom-desc").exists()).toBe(true); - }); - - it("uses custom illustration prop when provided", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - illustration: "i-mdi-custom-icon", - }, - }); - - expect(wrapper.find(".i-mdi-custom-icon").exists()).toBe(true); - }); - - describe("i18n integration", () => { - it("uses i18n translations when provided", () => { - const mockI18n = { - t: (key: string) => { - const translations: Record = { - "placeholder.empty.title": "Translated Empty Title", - "placeholder.empty.description": "Translated Empty Description", - "placeholder.error.title": "Translated Error Title", - "placeholder.error.description": "Translated Error Description", - }; - return translations[key] || key; - }, - locale: { value: "en" }, - }; - - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - global: { - provide: { - [INK_I18N_KEY as symbol]: mockI18n, - }, - }, - }); - - expect(wrapper.text()).toContain("Translated Empty Title"); - expect(wrapper.text()).toContain("Translated Empty Description"); - }); - - it("falls back to English when i18n not provided", () => { - const wrapper = mount(InkPlaceholder, { - props: { - state: "empty", - }, - }); - - expect(wrapper.text()).toContain("No data"); - expect(wrapper.text()).toContain("There's nothing here yet."); - }); - - it("uses i18n translations for error state", () => { - const mockI18n = { - t: (key: string) => { - const translations: Record = { - "placeholder.error.title": "Translated Error", - "placeholder.error.description": "Translated Error Description", - }; - return translations[key] || key; - }, - locale: { value: "en" }, - }; - - const wrapper = mount(InkPlaceholder, { - props: { - state: "error", - }, - global: { - provide: { - [INK_I18N_KEY as symbol]: mockI18n, - }, - }, - }); - - expect(wrapper.text()).toContain("Translated Error"); - expect(wrapper.text()).toContain("Translated Error Description"); - }); - }); -}); diff --git a/packages/web/src/components/inkPopup/inkPopup.ts b/packages/web/src/components/inkPopup/inkPopup.ts index af476a4..e68fe44 100644 --- a/packages/web/src/components/inkPopup/inkPopup.ts +++ b/packages/web/src/components/inkPopup/inkPopup.ts @@ -17,6 +17,7 @@ export const inkPopupProps = { default: "center", }, closeOnScrim: makeBooleanProp(true), + scrim: makeBooleanProp(true), } as const; // --- Emits --- diff --git a/packages/web/src/components/inkPopup/inkPopup.vue b/packages/web/src/components/inkPopup/inkPopup.vue index 1719894..55bfcc3 100644 --- a/packages/web/src/components/inkPopup/inkPopup.vue +++ b/packages/web/src/components/inkPopup/inkPopup.vue @@ -43,7 +43,12 @@ const onScrimClick = () => {