fix(app): resolve Bucket E.2-E.5 lint findings

WS-3 session 1b-ii Task 5b+c (audit Bucket E.2-E.5 — 6 items resolved,
2 promise/no-promise-in-callback warnings remain on dynamic-import
sites — see deviations).

This commit is split out from the originally-planned grouped Task 5
because the API stream timed out mid-session. E.1 (isAxiosError) is in
the preceding commit 0f155d9.

E.2 — vitest spec to Composition API (1× vue/component-api-style):
- useFormFailures.spec.ts: rewrote the test wrapper from
  \`{ setup() { return { result } }, render: () => h('div') }\`
  to \`setup(_, { expose }) { expose({ result }); return () => h('div') }\`.
  Pure Composition API: setup returns the render function; expose()
  declares the instance-visible \`result\` that the 7 \`vm.result.*\`
  assertions consume. Tests still pass green (49 tests).

E.3 — REAL BUG: missing return in computed (1× vue/return-in-computed-property):
- useTimeSlotDropdown.ts:80: the \`fetchParams\` computed had a switch
  over the \`DropdownScenario\` type (4 string-literal cases) without
  a \`default\` branch. If \`scenario.value\` ever returned a value
  outside the four narrowed cases (e.g. via a future type-assertion
  drift), the computed silently returned \`undefined\`, and the
  consumer code (\`fetchParams.value.includeParent\`) would throw
  \`Cannot read property 'includeParent' of undefined\`. Added a
  \`default\` branch returning \`{ includeParent: false, includeChildren: false }\`
  — same as the 'flat' case (the safest baseline: include only own
  slots, no hierarchy).

E.4 — SECURITY (1× vue/no-template-target-blank):
- pages/organisation/index.vue:343: the external website anchor had
  \`target='_blank'\` with \`rel='noopener'\` (only one). The rule
  requires the full \`rel='noopener noreferrer'\` pair. Updated.
  Mitigates reverse-tabnabbing (window.opener) AND referrer-leakage
  to the linked third-party site.

E.5 — axios fire-and-forget (3× promise/no-promise-in-callback,
1 fully resolved + 2 warnings remain):
- lib/axios.ts:42: changed \`error => Promise.reject(error)\` to
  \`async error => { throw error }\`. Semantically identical (axios
  interceptor onRejected returns a rejected promise either way) and
  satisfies the lint rule.
- lib/axios.ts:61, 73: prefixed the dynamic-import chains with \`void\`
  per Q4's option-a decision (\`void import('@/stores/...').then(...)\`).
  This makes the discard intent explicit, but empirically does NOT
  satisfy promise/no-promise-in-callback — the rule fires on any
  promise creation inside a callback, regardless of the discard
  pattern. The 2 warnings remain in the post-Task-5 baseline.
  Resolution path is Bert's call: either keep \`void\` and accept
  the warnings as documentation, or rewrite to \`async error => {
  const { useStore } = await import(...); ... }\` which sequentializes
  the dynamic-import resolution with the rejection. Out of scope for
  this session per the literal Q4 recipe.

Tests + typecheck verified green.

Lint baseline: 34 → 32.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 15:15:29 +02:00
parent 0f155d9e5d
commit 1289b217d0
4 changed files with 11 additions and 7 deletions

View File

@@ -30,12 +30,13 @@ function mountWithQuery<T>(setup: () => T): { vm: { result: T }; client: QueryCl
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const Component = defineComponent({
setup() {
setup(_props, { expose }) {
const result = setup()
return { result }
expose({ result })
return () => h('div')
},
render: () => h('div'),
})
const wrapper = mount(Component, {

View File

@@ -88,6 +88,9 @@ export function useTimeSlotDropdown(
case 'cross_event':
return { includeParent: false, includeChildren: true }
default:
return { includeParent: false, includeChildren: false }
}
})

View File

@@ -39,7 +39,7 @@ apiClient.interceptors.request.use(
return config
},
error => Promise.reject(error),
async error => { throw error },
)
apiClient.interceptors.response.use(
@@ -58,7 +58,7 @@ apiClient.interceptors.response.use(
// Handle impersonation session expiry
if (status === 403 && error.response?.data?.impersonation_ended) {
import('@/stores/useImpersonationStore').then(({ useImpersonationStore }) => {
void import('@/stores/useImpersonationStore').then(({ useImpersonationStore }) => {
const impersonationStore = useImpersonationStore()
impersonationStore.clearState()
@@ -70,7 +70,7 @@ apiClient.interceptors.response.use(
if (status === 401) {
// Lazy import to avoid circular dependency
import('@/stores/useAuthStore').then(({ useAuthStore }) => {
void import('@/stores/useAuthStore').then(({ useAuthStore }) => {
const authStore = useAuthStore()
if (authStore.isInitialized)
authStore.handleUnauthorized()

View File

@@ -340,7 +340,7 @@ function describeActivity(entry: ActivityLogEntry): string {
v-if="organisation.website"
:href="organisation.website"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
class="text-body-1 text-primary"
>{{ organisation.website }}</a>
<span