This is an automated email from the ASF dual-hosted git repository. yasithdev pushed a commit to branch feat/generic-experiment-launcher in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit c5f97a3a426cc0dea70a1366e0d29179835f6e43 Author: yasithdev <[email protected]> AuthorDate: Fri Apr 24 22:21:53 2026 -0400 feat(launcher): WizardTabs with strict-forward gating --- .../js/components/launch/WizardTabs.vue | 37 ++++++++++++++++++++ .../unit/components/launch/WizardTabs.spec.ts | 40 ++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/WizardTabs.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/WizardTabs.vue new file mode 100644 index 000000000..7d2b4f76d --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/WizardTabs.vue @@ -0,0 +1,37 @@ +<template> + <div class="nav nav-tabs mb-3" role="tablist"> + <button + v-for="t in tabs" + :key="t.idx" + role="tab" + type="button" + class="nav-link" + :class="{ active: t.idx === active }" + :disabled="t.disabled" + @click="onClick(t.idx, t.disabled)" + > + {{ t.idx }} ยท {{ t.label }} + </button> + </div> +</template> + +<script setup lang="ts"> +import { computed } from "vue"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; + +defineProps<{ active: 1 | 2 | 3 }>(); +const emit = defineEmits<{ "update:active": [n: 1 | 2 | 3] }>(); + +const store = useLaunchStore(); + +const tabs = computed(() => [ + { idx: 1 as const, label: "Application & Inputs", disabled: false }, + { idx: 2 as const, label: "Runtime", disabled: !store.tab1Valid }, + { idx: 3 as const, label: "Review & Launch", disabled: !store.tab1Valid || !store.tab2Valid }, +]); + +function onClick(n: 1 | 2 | 3, disabled: boolean) { + if (disabled) return; + emit("update:active", n); +} +</script> diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/WizardTabs.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/WizardTabs.spec.ts new file mode 100644 index 000000000..41ebbab5f --- /dev/null +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/components/launch/WizardTabs.spec.ts @@ -0,0 +1,40 @@ +import { mount } from "@vue/test-utils"; +import { setActivePinia, createPinia } from "pinia"; +import { beforeEach, describe, expect, it } from "vitest"; +import WizardTabs from "../../../../js/components/launch/WizardTabs.vue"; +import { useLaunchStore } from "django-airavata-common-ui/js/stores/launch"; + +describe("WizardTabs", () => { + beforeEach(() => setActivePinia(createPinia())); + + function makeMount(active: 1 | 2 | 3 = 1) { + return mount(WizardTabs, { props: { active }, global: { stubs: { transition: false } } }); + } + + it("renders three tab buttons", () => { + const w = makeMount(); + expect(w.findAll("button[role='tab']")).toHaveLength(3); + }); + + it("disables tabs 2 and 3 when tab 1 is invalid", () => { + const w = makeMount(); + const tabs = w.findAll("button[role='tab']"); + expect(tabs[1].attributes("disabled")).toBeDefined(); + expect(tabs[2].attributes("disabled")).toBeDefined(); + }); + + it("emits update:active when allowed tab clicked", async () => { + const store = useLaunchStore(); + // Force tab 1 valid by spying on the getter + Object.defineProperty(store, "tab1Valid", { value: true }); + const w = makeMount(); + await w.findAll("button[role='tab']")[1].trigger("click"); + expect(w.emitted("update:active")?.[0]).toEqual([2]); + }); + + it("does not emit update:active for a disabled tab", async () => { + const w = makeMount(); + await w.findAll("button[role='tab']")[1].trigger("click"); + expect(w.emitted("update:active")).toBeUndefined(); + }); +});
