diff --git a/agdb_studio/package-lock.json b/agdb_studio/package-lock.json index ede0229d8..8d4c4da76 100644 --- a/agdb_studio/package-lock.json +++ b/agdb_studio/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@kalimahapps/vue-icons": "^1.7.1", + "@vueuse/components": "^12.0.0", + "@vueuse/core": "^12.0.0", "agdb_api": "file:../agdb_api/typescript", "openapi-client-axios": "^7.5.1", "vue": "^3.3.4", @@ -1305,6 +1307,11 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", @@ -1699,12 +1706,12 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", - "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.12", + "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" @@ -1716,27 +1723,27 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", - "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", "dependencies": { - "@vue/compiler-core": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", - "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.12", - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", - "postcss": "^8.4.47", + "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, @@ -1746,12 +1753,12 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", - "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/compiler-vue2": { @@ -1832,49 +1839,49 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", - "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "dependencies": { - "@vue/shared": "3.5.12" + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", - "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", - "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/runtime-core": "3.5.12", - "@vue/shared": "3.5.12", + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", - "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", "dependencies": { - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { - "vue": "3.5.12" + "vue": "3.5.13" } }, "node_modules/@vue/shared": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", - "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==" + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" }, "node_modules/@vue/test-utils": { "version": "2.4.6", @@ -1892,6 +1899,49 @@ "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", "dev": true }, + "node_modules/@vueuse/components": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-12.0.0.tgz", + "integrity": "sha512-XpOoBXYRuFuUiiq+HsMX6rGzqvcHdKnbT4sbR0FHYxwSGBHO3Zli8pPTZoLRNBGp4CGov7BRCnANEK/1Ch/6tQ==", + "dependencies": { + "@vueuse/core": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + } + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -4115,9 +4165,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -4134,7 +4184,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -5065,15 +5115,15 @@ "dev": true }, "node_modules/vue": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", - "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", - "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-sfc": "3.5.12", - "@vue/runtime-dom": "3.5.12", - "@vue/server-renderer": "3.5.12", - "@vue/shared": "3.5.12" + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" diff --git a/agdb_studio/package.json b/agdb_studio/package.json index a64f189fd..ca3477a4c 100644 --- a/agdb_studio/package.json +++ b/agdb_studio/package.json @@ -17,6 +17,8 @@ }, "dependencies": { "@kalimahapps/vue-icons": "^1.7.1", + "@vueuse/components": "^12.0.0", + "@vueuse/core": "^12.0.0", "agdb_api": "file:../agdb_api/typescript", "openapi-client-axios": "^7.5.1", "vue": "^3.3.4", diff --git a/agdb_studio/src/assets/base.css b/agdb_studio/src/assets/base.css index 37518dcf1..8e480440e 100644 --- a/agdb_studio/src/assets/base.css +++ b/agdb_studio/src/assets/base.css @@ -22,12 +22,15 @@ --text-dark-2: rgba(235, 235, 235, 0.64); --base-font: "Red Hat Display", sans-serif; + + --orange: #ffa02c; } :root { --color-background: var(--white); --color-background-soft: var(--white-soft); --color-background-mute: var(--white-mute); + --color-background-active: var(--orange); --color-border: var(--divider-light-2); --color-border-hover: var(--divider-light-1); diff --git a/agdb_studio/src/assets/button.less b/agdb_studio/src/assets/button.less index 9fcc8a14f..b988ce18a 100644 --- a/agdb_studio/src/assets/button.less +++ b/agdb_studio/src/assets/button.less @@ -21,25 +21,25 @@ .button-default { --backgroundColor: #007bff; - --color: #fff; + --color: var(--white); --borderColor: #025ec0; } .button-success { --backgroundColor: #28a745; - --color: #fff; + --color: var(--white); --borderColor: #1e7732; } .button-warning { - --backgroundColor: #ffa02c; - --color: #181818; + --backgroundColor: var(--orange); + --color: var(--black); --borderColor: #d88621; } .button-danger { --backgroundColor: #dc3545; - --color: #fff; + --color: var(--white); --borderColor: #af2836; } diff --git a/agdb_studio/src/components/base/dropdown/AgdbDropdown.spec.ts b/agdb_studio/src/components/base/dropdown/AgdbDropdown.spec.ts new file mode 100644 index 000000000..e39811c63 --- /dev/null +++ b/agdb_studio/src/components/base/dropdown/AgdbDropdown.spec.ts @@ -0,0 +1,44 @@ +import { mount } from "@vue/test-utils"; +import AgdbDropdown from "./AgdbDropdown.vue"; +import { describe, beforeEach, vi, it, expect } from "vitest"; + +describe("AgdbDropdown", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it("should open and close on click", async () => { + const wrapper = mount(AgdbDropdown, { + slots: { + content: "
content
", + trigger: "
trigger
", + }, + }); + const trigger = wrapper.find(".trigger"); + expect(wrapper.find(".content").exists()).toBe(false); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").isVisible()).toBe(true); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").exists()).toBe(false); + }); + + it("should close when clicking outside", async () => { + const wrapper = mount(AgdbDropdown, { + slots: { + content: "
content
", + trigger: "
trigger
", + }, + }); + const trigger = wrapper.find(".trigger"); + expect(wrapper.find(".content").exists()).toBe(false); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").isVisible()).toBe(true); + document.body.click(); + await wrapper.vm.$nextTick(); + document.body.click(); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").exists()).toBe(false); + }); +}); diff --git a/agdb_studio/src/components/base/dropdown/AgdbDropdown.vue b/agdb_studio/src/components/base/dropdown/AgdbDropdown.vue new file mode 100644 index 000000000..4fdb87f0a --- /dev/null +++ b/agdb_studio/src/components/base/dropdown/AgdbDropdown.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/agdb_studio/src/components/base/menu/AgdbMenu.spec.ts b/agdb_studio/src/components/base/menu/AgdbMenu.spec.ts new file mode 100644 index 000000000..d3565cdb4 --- /dev/null +++ b/agdb_studio/src/components/base/menu/AgdbMenu.spec.ts @@ -0,0 +1,43 @@ +import { describe, beforeEach, vi, it, expect } from "vitest"; +import { mount } from "@vue/test-utils"; +import AgdbMenu from "./AgdbMenu.vue"; +import { dbActions } from "@/composables/db/dbConfig"; +import { db_backup } from "@/tests/apiMock"; + +describe("AgdbMenu", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it("should run action on click", () => { + const wrapper = mount(AgdbMenu, { + props: { + actions: dbActions, + }, + }); + + expect(wrapper.find(".agdb-menu").exists()).toBe(true); + expect(wrapper.find(".agdb-menu").text()).toContain("Convert"); + + const backup = wrapper.find(".menu-item[data-key='backup']"); + backup.trigger("click"); + expect(db_backup).toHaveBeenCalled(); + }); + + it("should render the sub menu on hover", async () => { + const wrapper = mount(AgdbMenu, { + props: { + actions: dbActions, + }, + }); + + const convert = wrapper.find(".menu-item[data-key='convert']"); + await convert.trigger("mouseover"); + + expect(wrapper.find(".sub-menu").exists()).toBe(true); + expect(wrapper.find(".sub-menu").text()).toContain("Memory"); + + await wrapper.trigger("mouseleave"); + + expect(wrapper.find(".sub-menu").exists()).toBe(false); + }); +}); diff --git a/agdb_studio/src/components/base/menu/AgdbMenu.vue b/agdb_studio/src/components/base/menu/AgdbMenu.vue new file mode 100644 index 000000000..6f778a9ba --- /dev/null +++ b/agdb_studio/src/components/base/menu/AgdbMenu.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/agdb_studio/src/components/base/table/AgdbCell.spec.ts b/agdb_studio/src/components/base/table/AgdbCell.spec.ts new file mode 100644 index 000000000..e3049783e --- /dev/null +++ b/agdb_studio/src/components/base/table/AgdbCell.spec.ts @@ -0,0 +1,151 @@ +import { describe, beforeEach, vi, it, expect } from "vitest"; +import { mount, shallowMount } from "@vue/test-utils"; +import AgdbCell from "./AgdbCell.vue"; +import { + INJECT_KEY_COLUMNS, + INJECT_KEY_ROW, +} from "@/composables/table/constants"; +import { columnsMap } from "@/tests/tableMocks"; + +describe("AgdbCell", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it("should render the cell", () => { + const wrapper = mount(AgdbCell, { + props: { + cellKey: "owner", + }, + global: { + provide: { + [INJECT_KEY_COLUMNS]: { value: columnsMap }, + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + + expect(wrapper.find(".agdb-cell").exists()).toBe(true); + expect(wrapper.find(".agdb-cell").text()).toBe("admin"); + }); + + it("should render the cell with a formatter", () => { + const wrapper = mount(AgdbCell, { + props: { + cellKey: "backup", + }, + global: { + provide: { + [INJECT_KEY_COLUMNS]: { value: columnsMap }, + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 123456, + }, + }, + }, + }, + }); + + expect(wrapper.find(".agdb-cell").exists()).toBe(true); + expect(wrapper.find(".agdb-cell").text()).toBe("1"); + }); + + it("should handle missing row data", () => { + const wrapper = mount(AgdbCell, { + props: { + cellKey: "backup", + }, + global: { + provide: { + [INJECT_KEY_COLUMNS]: { value: columnsMap }, + [INJECT_KEY_ROW]: undefined, + }, + }, + }); + + expect(wrapper.find(".agdb-cell").exists()).toBe(true); + expect(wrapper.find(".agdb-cell").text()).toBe("0"); + }); + + it("should display custom component", () => { + const columns = new Map(); + columns.set("owner", { + key: "owner", + title: "Owner", + cellComponent: { + template: "
Custom component
", + }, + }); + const wrapper = mount(AgdbCell, { + props: { + cellKey: "owner", + }, + global: { + provide: { + [INJECT_KEY_COLUMNS]: { value: columns }, + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + + expect(wrapper.find(".agdb-cell").exists()).toBe(true); + expect(wrapper.find(".agdb-cell").text()).toBe("Custom component"); + }); + it("should display menu", async () => { + const columns = new Map(); + columns.set("actions", { + key: "actions", + title: "Actions", + actions: [ + { + key: "backup", + title: "Backup", + action: () => {}, + }, + ], + }); + const wrapper = shallowMount(AgdbCell, { + props: { + cellKey: "actions", + }, + global: { + provide: { + [INJECT_KEY_COLUMNS]: { value: columns }, + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + expect(wrapper.html()).toContain("agdb-cell-menu-stub"); + }); +}); diff --git a/agdb_studio/src/components/base/table/AgdbCell.vue b/agdb_studio/src/components/base/table/AgdbCell.vue new file mode 100644 index 000000000..b9627d3cd --- /dev/null +++ b/agdb_studio/src/components/base/table/AgdbCell.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts b/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts new file mode 100644 index 000000000..507f742d5 --- /dev/null +++ b/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts @@ -0,0 +1,114 @@ +import { mount } from "@vue/test-utils"; +import AgdbCellMenu from "./AgdbCellMenu.vue"; +import { describe, beforeEach, vi, it, expect } from "vitest"; +import { dbActions } from "@/composables/db/dbConfig"; +import { INJECT_KEY_ROW } from "@/composables/table/constants"; +const { fetchDatabases } = vi.hoisted(() => { + return { + fetchDatabases: vi.fn(), + }; +}); + +vi.mock("@/composables/db/dbStore", () => { + return { + useDbStore: () => { + return { + fetchDatabases, + }; + }, + }; +}); +describe("AgdbCellMenu", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it("should open and close on click", async () => { + const wrapper = mount(AgdbCellMenu, { + props: { + actions: dbActions, + }, + global: { + provide: { + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + const trigger = wrapper.find(".trigger"); + expect(wrapper.find(".content").exists()).toBe(false); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").isVisible()).toBe(true); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").exists()).toBe(false); + }); + it("should call action on click", async () => { + const wrapper = mount(AgdbCellMenu, { + props: { + actions: dbActions, + }, + global: { + provide: { + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + const trigger = wrapper.find(".trigger"); + expect(wrapper.find(".content").exists()).toBe(false); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").isVisible()).toBe(true); + const action = wrapper.find(".menu-item[data-key=backup]"); + await action.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").exists()).toBe(false); + }); + it("should not close the dropdown if item has no action", async () => { + const wrapper = mount(AgdbCellMenu, { + props: { + actions: dbActions, + }, + global: { + provide: { + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + const trigger = wrapper.find(".trigger"); + expect(wrapper.find(".content").exists()).toBe(false); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").isVisible()).toBe(true); + const action = wrapper.find(".menu-item[data-key=convert]"); + await action.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").exists()).toBe(true); + }); +}); diff --git a/agdb_studio/src/components/base/table/AgdbCellMenu.vue b/agdb_studio/src/components/base/table/AgdbCellMenu.vue new file mode 100644 index 000000000..da478921a --- /dev/null +++ b/agdb_studio/src/components/base/table/AgdbCellMenu.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/agdb_studio/src/components/base/table/AgdbTable.spec.ts b/agdb_studio/src/components/base/table/AgdbTable.spec.ts index c46b16341..49fd96c57 100644 --- a/agdb_studio/src/components/base/table/AgdbTable.spec.ts +++ b/agdb_studio/src/components/base/table/AgdbTable.spec.ts @@ -3,13 +3,14 @@ import AgdbTable from "./AgdbTable.vue"; import { addTable, clearTables } from "@/composables/table/tableConfig"; import { setTableData } from "@/composables/table/tableData"; import { TABLE_NAME, tableConfig, tableData } from "@/tests/tableMocks"; +import { describe, beforeEach, it, expect } from "vitest"; describe("AgdbTable", () => { beforeEach(() => { clearTables(); }); it("should render for correct data", () => { - addTable({ name: TABLE_NAME, columns: tableConfig, uniqueKey: "name" }); + addTable({ name: TABLE_NAME, columns: tableConfig }); setTableData(TABLE_NAME, tableData); const wrapper = mount(AgdbTable, { diff --git a/agdb_studio/src/components/base/table/AgdbTable.vue b/agdb_studio/src/components/base/table/AgdbTable.vue index 23abdc5ad..aaf0d3b55 100644 --- a/agdb_studio/src/components/base/table/AgdbTable.vue +++ b/agdb_studio/src/components/base/table/AgdbTable.vue @@ -1,10 +1,14 @@ @@ -46,5 +53,6 @@ const columns = computed(() => { gap: 1rem; padding: 0.5rem; border-bottom: 1px solid var(--color-border); + white-space: nowrap; } diff --git a/agdb_studio/src/components/base/table/TableHeader.spec.ts b/agdb_studio/src/components/base/table/AgdbTableHeader.spec.ts similarity index 61% rename from agdb_studio/src/components/base/table/TableHeader.spec.ts rename to agdb_studio/src/components/base/table/AgdbTableHeader.spec.ts index 4bd8da812..c022c9724 100644 --- a/agdb_studio/src/components/base/table/TableHeader.spec.ts +++ b/agdb_studio/src/components/base/table/AgdbTableHeader.spec.ts @@ -1,9 +1,10 @@ import { shallowMount } from "@vue/test-utils"; -import TableHeader from "./TableHeader.vue"; +import AgdbTableHeader from "./AgdbTableHeader.vue"; +import { describe, it, expect } from "vitest"; describe("TableHeader", () => { it("should render", () => { - const wrapper = shallowMount(TableHeader, { + const wrapper = shallowMount(AgdbTableHeader, { props: { tableKey: "table", }, diff --git a/agdb_studio/src/components/base/table/TableHeader.vue b/agdb_studio/src/components/base/table/AgdbTableHeader.vue similarity index 93% rename from agdb_studio/src/components/base/table/TableHeader.vue rename to agdb_studio/src/components/base/table/AgdbTableHeader.vue index c1b8aaaff..1cb0ddb06 100644 --- a/agdb_studio/src/components/base/table/TableHeader.vue +++ b/agdb_studio/src/components/base/table/AgdbTableHeader.vue @@ -1,6 +1,6 @@ diff --git a/agdb_studio/src/components/base/table/TableRow.spec.ts b/agdb_studio/src/components/base/table/TableRow.spec.ts deleted file mode 100644 index 7c1eae1ce..000000000 --- a/agdb_studio/src/components/base/table/TableRow.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { shallowMount } from "@vue/test-utils"; -import TableRow from "./TableRow.vue"; -import { columnsMap } from "@/tests/tableMocks"; - -describe("TableRow", () => { - it("should render", () => { - const wrapper = shallowMount(TableRow, { - props: { - columns: columnsMap, - row: { - role: "admin", - name: "admin/app3", - db_type: "file", - size: 50, - backup: 0, - }, - }, - }); - expect(wrapper.text()).toContain("admin"); - }); -}); diff --git a/agdb_studio/src/components/db/DbAddForm.spec.ts b/agdb_studio/src/components/db/DbAddForm.spec.ts index 5bab20ffb..e3e390452 100644 --- a/agdb_studio/src/components/db/DbAddForm.spec.ts +++ b/agdb_studio/src/components/db/DbAddForm.spec.ts @@ -1,5 +1,6 @@ import DbAddForm from "./DbAddForm.vue"; import { mount } from "@vue/test-utils"; +import { describe, beforeEach, vi, it, expect } from "vitest"; const { addDatabase } = vi.hoisted(() => { return { @@ -7,9 +8,9 @@ const { addDatabase } = vi.hoisted(() => { }; }); -vi.mock("@/composables/stores/DbStore", () => { +vi.mock("@/composables/db/dbStore", () => { return { - useDbList: () => { + useDbStore: () => { return { addDatabase, }; @@ -22,6 +23,7 @@ describe("DbAddForm", () => { vi.clearAllMocks(); }); it("should add a database when user submits", async () => { + addDatabase.mockResolvedValueOnce(true); expect(addDatabase).not.toHaveBeenCalled(); const wrapper = mount(DbAddForm); await wrapper.find("input").setValue("test_db"); @@ -31,6 +33,7 @@ describe("DbAddForm", () => { expect(addDatabase).toHaveBeenCalledOnce(); }); it("should add a database when user clicks submit button", async () => { + addDatabase.mockResolvedValueOnce(true); expect(addDatabase).not.toHaveBeenCalled(); const wrapper = mount(DbAddForm); await wrapper.find("input").setValue("test_db"); diff --git a/agdb_studio/src/components/db/DbAddForm.vue b/agdb_studio/src/components/db/DbAddForm.vue index dc8f92c5d..ec284a498 100644 --- a/agdb_studio/src/components/db/DbAddForm.vue +++ b/agdb_studio/src/components/db/DbAddForm.vue @@ -1,18 +1,32 @@ diff --git a/agdb_studio/src/components/db/DbList.spec.ts b/agdb_studio/src/components/db/DbList.spec.ts deleted file mode 100644 index b13b300ac..000000000 --- a/agdb_studio/src/components/db/DbList.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import DbList from "./DbList.vue"; -import { mount, shallowMount } from "@vue/test-utils"; - -const { databases, fetchDatabases } = vi.hoisted(() => { - return { - databases: [ - { - name: "test_db", - db_type: "memory", - role: "admin", - size: 2656, - backup: 0, - }, - { - name: "test_db2", - db_type: "memory", - role: "admin", - size: 2656, - backup: 0, - }, - ], - - fetchDatabases: vi.fn(), - }; -}); - -vi.mock("@/composables/stores/DbStore", () => { - return { - useDbList: () => { - return { - databases, - fetchDatabases, - }; - }, - }; -}); - -describe("DbList", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it("should render databases when the page view loads", () => { - const wrapper = shallowMount(DbList); - expect(wrapper.html()).toContain("db-add-form-stub"); - expect(wrapper.html()).toContain("db-table-stub"); - }); - it("should fetch databases when the page view loads", () => { - expect(fetchDatabases).not.toHaveBeenCalled(); - mount(DbList); - expect(fetchDatabases).toHaveBeenCalledOnce(); - }); - it("should render a message when there are no databases", () => { - databases.length = 0; - const wrapper = mount(DbList); - expect(wrapper.text()).toContain("No databases found"); - }); - it("should refresh databases when user clicks refresh button", async () => { - expect(fetchDatabases).not.toHaveBeenCalled(); - const wrapper = mount(DbList); - await wrapper.find("button").trigger("click"); - await wrapper.vm.$nextTick(); - expect(fetchDatabases).toHaveBeenCalledTimes(2); - }); -}); diff --git a/agdb_studio/src/components/db/DbList.vue b/agdb_studio/src/components/db/DbList.vue deleted file mode 100644 index d09e05076..000000000 --- a/agdb_studio/src/components/db/DbList.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/agdb_studio/src/components/db/DbTable.spec.ts b/agdb_studio/src/components/db/DbTable.spec.ts index 6dcd4ce53..2e34c2522 100644 --- a/agdb_studio/src/components/db/DbTable.spec.ts +++ b/agdb_studio/src/components/db/DbTable.spec.ts @@ -1,5 +1,6 @@ import { shallowMount } from "@vue/test-utils"; import DbTable from "./DbTable.vue"; +import { describe, it, expect } from "vitest"; describe("DbTable", () => { it("should render", () => { diff --git a/agdb_studio/src/components/db/DbTable.vue b/agdb_studio/src/components/db/DbTable.vue index a90169113..3fb2efc2b 100644 --- a/agdb_studio/src/components/db/DbTable.vue +++ b/agdb_studio/src/components/db/DbTable.vue @@ -1,29 +1,18 @@ - + diff --git a/agdb_studio/vitest.config.mts b/agdb_studio/vitest.config.mts index c62b3846b..24e1c8bb0 100644 --- a/agdb_studio/vitest.config.mts +++ b/agdb_studio/vitest.config.mts @@ -1,4 +1,3 @@ -// import { fileURLToPath } from "node:url"; import { mergeConfig, defineConfig, @@ -15,7 +14,6 @@ export default mergeConfig( environment: "jsdom", exclude: [...configDefaults.exclude, "e2e/*"], root: path.resolve(__dirname, "."), - // root: fileURLToPath(new URL("./", import.meta.url)), coverage: { provider: "istanbul", all: true,