feat: mirror v-can guard chains
This commit is contained in:
21
test/chains.test.ts
Normal file
21
test/chains.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { setup, $fetch } from '@nuxt/test-utils/e2e'
|
||||
|
||||
describe('chains fixture', async () => {
|
||||
await setup({
|
||||
rootDir: fileURLToPath(new URL('./fixtures/chains', import.meta.url)),
|
||||
})
|
||||
|
||||
it('hides every branch when permission is missing and falls back to v-cannot', async () => {
|
||||
const html = await $fetch('/')
|
||||
expect(html).not.toContain('id="branch-pending"')
|
||||
expect(html).toContain('id="branch-denied"')
|
||||
})
|
||||
|
||||
it('renders explicit v-cannot blocks even when detached from the chain', async () => {
|
||||
const html = await $fetch('/')
|
||||
expect(html).toContain('id="explicit-denied"')
|
||||
expect(html).not.toContain('id="approve-action"')
|
||||
})
|
||||
})
|
||||
4
test/fixtures/basic/permissions/__can__.ts
vendored
4
test/fixtures/basic/permissions/__can__.ts
vendored
@@ -4,7 +4,7 @@ const granted = new Set([
|
||||
'contract.create',
|
||||
])
|
||||
|
||||
export function __can__(path: string[]) {
|
||||
const key = Array.isArray(path) ? path.join('.') : String(path)
|
||||
export function __can__(...path: string[]) {
|
||||
const key = path.join('.')
|
||||
return granted.has(key)
|
||||
}
|
||||
|
||||
31
test/fixtures/chains/app.vue
vendored
Normal file
31
test/fixtures/chains/app.vue
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<main>
|
||||
<section>
|
||||
<div id="branch-draft" v-if="phase === 'draft'" v-can="can.employee.view">
|
||||
Draft
|
||||
</div>
|
||||
<div id="branch-pending" v-else-if="phase === 'pending'">
|
||||
Pending
|
||||
</div>
|
||||
<div id="branch-fallback" v-else>
|
||||
Fallback
|
||||
</div>
|
||||
<p id="branch-denied" v-cannot>
|
||||
Permission missing
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button id="approve-action" v-can="can.contract.approve">
|
||||
Approve
|
||||
</button>
|
||||
<p id="explicit-denied" v-cannot="can.contract.approve">
|
||||
Need contract.approve
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const phase = 'pending'
|
||||
</script>
|
||||
12
test/fixtures/chains/nuxt.config.ts
vendored
Normal file
12
test/fixtures/chains/nuxt.config.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import NuxtCan from '../../../src/module'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: [NuxtCan],
|
||||
nuxtCan: {
|
||||
permissions: {
|
||||
employee: ['view'],
|
||||
contract: ['approve'],
|
||||
},
|
||||
canFunctionImport: '~/permissions/__can__',
|
||||
},
|
||||
})
|
||||
5
test/fixtures/chains/package.json
vendored
Normal file
5
test/fixtures/chains/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "chains",
|
||||
"type": "module"
|
||||
}
|
||||
6
test/fixtures/chains/permissions/__can__.ts
vendored
Normal file
6
test/fixtures/chains/permissions/__can__.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
const granted = new Set<string>([])
|
||||
|
||||
export function __can__(...path: string[]) {
|
||||
const key = path.join('.')
|
||||
return granted.has(key)
|
||||
}
|
||||
@@ -18,8 +18,8 @@ describe('transformCan', () => {
|
||||
<p v-cannot>Refus</p>
|
||||
`)
|
||||
|
||||
expect(code).toContain(`v-if="__can__(['employee', 'view'])"`)
|
||||
expect(code).toContain(`v-if="!(__can__(['employee', 'view']))"`)
|
||||
expect(code).toContain(`v-if="__can__('employee', 'view')"`)
|
||||
expect(code).toContain(`v-if="!(__can__('employee', 'view'))"`)
|
||||
})
|
||||
|
||||
it('merges existing v-if expressions with the generated guard', () => {
|
||||
@@ -27,7 +27,7 @@ describe('transformCan', () => {
|
||||
<div v-if="isReady" v-can="can.contract.create" />
|
||||
`)
|
||||
|
||||
expect(code).toContain(`v-if="(isReady) && __can__(['contract', 'create'])"`)
|
||||
expect(code).toContain(`v-if="(isReady) && __can__('contract', 'create')"`)
|
||||
})
|
||||
|
||||
it('throws when v-cannot is used without a preceding v-can', () => {
|
||||
@@ -36,7 +36,7 @@ describe('transformCan', () => {
|
||||
id: TEST_FILE,
|
||||
})
|
||||
|
||||
expect(exec).toThrow(/must immediately follow its `v-can`/)
|
||||
expect(exec).toThrow(/without an expression must immediately follow/)
|
||||
})
|
||||
|
||||
it('throws when the expression does not start with can.*', () => {
|
||||
@@ -48,15 +48,61 @@ describe('transformCan', () => {
|
||||
expect(exec).toThrow(/expressions must start with `can\.`/)
|
||||
})
|
||||
|
||||
it('throws when v-can is added to a v-else branch', () => {
|
||||
it('mirrors guards across v-if / v-else-if / v-else chains when the first branch uses v-can', () => {
|
||||
const code = runTransform(`
|
||||
<div v-if="ready" v-can="can.employee.view"></div>
|
||||
<div v-else-if="later"></div>
|
||||
<div v-else></div>
|
||||
<p v-cannot>Denied</p>
|
||||
`)
|
||||
|
||||
expect(code).toContain(`v-if="(ready) && __can__('employee', 'view')"`)
|
||||
expect(code).toContain(`v-else-if="(later) && __can__('employee', 'view')"`)
|
||||
expect(code).toContain(`v-else-if="__can__('employee', 'view')"`)
|
||||
expect(code).toContain(`v-if="!(__can__('employee', 'view'))"`)
|
||||
})
|
||||
|
||||
it('throws when branches declare different v-can expressions', () => {
|
||||
const exec = () => transformCan({
|
||||
code: buildSFC(`
|
||||
<div v-if="ready"></div>
|
||||
<div v-else v-can="can.employee.view"></div>
|
||||
<div v-if="ready" v-can="can.employee.view"></div>
|
||||
<div v-else v-can="can.contract.create"></div>
|
||||
`),
|
||||
id: TEST_FILE,
|
||||
})
|
||||
|
||||
expect(exec).toThrow(/cannot be used on `v-else`/)
|
||||
expect(exec).toThrow(/must match across every branch/)
|
||||
})
|
||||
|
||||
it('allows explicit expressions on v-cannot without a preceding v-can', () => {
|
||||
const code = runTransform('<p v-cannot="can.employee.view">Denied</p>')
|
||||
expect(code).toContain(`v-if="!(__can__('employee', 'view'))"`)
|
||||
})
|
||||
|
||||
it('requires adjacent placement for v-cannot without an expression', () => {
|
||||
const exec = () => transformCan({
|
||||
code: buildSFC(`
|
||||
<div v-can="can.employee.view">Allowed</div>
|
||||
<div>
|
||||
<p v-cannot>Denied</p>
|
||||
</div>
|
||||
`),
|
||||
id: TEST_FILE,
|
||||
})
|
||||
|
||||
expect(exec).toThrow(/without an expression must immediately follow/)
|
||||
})
|
||||
|
||||
it('requires adjacency even when only empty siblings exist between v-can and v-cannot', () => {
|
||||
const exec = () => transformCan({
|
||||
code: buildSFC(`
|
||||
<div v-can="can.employee.view">Allowed</div>
|
||||
<div></div>
|
||||
<div v-cannot>Denied</div>
|
||||
`),
|
||||
id: TEST_FILE,
|
||||
})
|
||||
|
||||
expect(exec).toThrow(/without an expression must immediately follow/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user