218 lines
6.4 KiB
JavaScript
218 lines
6.4 KiB
JavaScript
/**
|
|
* Property-based tests for unlock functionality
|
|
* **Feature: miniapp-frontend, Property 14: Unlock Quota Check**
|
|
* **Validates: Requirements 6.4**
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import * as fc from 'fast-check'
|
|
|
|
/**
|
|
* Helper functions for unlock validation
|
|
* These mirror the validation logic in the unlock popup and profile detail page
|
|
*/
|
|
|
|
/**
|
|
* Determine if unlock should proceed or show membership prompt
|
|
* Property 14: Unlock Quota Check
|
|
* @param {number} remainingQuota - Remaining unlock quota
|
|
* @returns {string} Action result: 'proceed_unlock' or 'show_member_prompt'
|
|
*/
|
|
export function getUnlockActionResult(remainingQuota) {
|
|
if (remainingQuota <= 0) {
|
|
return 'show_member_prompt'
|
|
}
|
|
return 'proceed_unlock'
|
|
}
|
|
|
|
/**
|
|
* Determine if unlock button should be shown vs member button
|
|
* @param {number} remainingQuota - Remaining unlock quota
|
|
* @returns {boolean} Whether to show unlock button (true) or member button (false)
|
|
*/
|
|
export function shouldShowUnlockButton(remainingQuota) {
|
|
return remainingQuota > 0
|
|
}
|
|
|
|
/**
|
|
* Determine if member purchase prompt should be shown
|
|
* @param {number} remainingQuota - Remaining unlock quota
|
|
* @returns {boolean} Whether to show member purchase prompt
|
|
*/
|
|
export function shouldShowMemberPrompt(remainingQuota) {
|
|
return remainingQuota <= 0
|
|
}
|
|
|
|
/**
|
|
* Calculate remaining quota after unlock
|
|
* @param {number} currentQuota - Current remaining quota
|
|
* @returns {number} New remaining quota after unlock
|
|
*/
|
|
export function calculateRemainingQuotaAfterUnlock(currentQuota) {
|
|
return Math.max(0, currentQuota - 1)
|
|
}
|
|
|
|
describe('Unlock Property Tests', () => {
|
|
/**
|
|
* Property 14: Unlock Quota Check
|
|
* *For any* unlock attempt where remaining quota is 0,
|
|
* a membership purchase prompt SHALL be displayed instead of proceeding with unlock.
|
|
* **Validates: Requirements 6.4**
|
|
*/
|
|
describe('Property 14: Unlock Quota Check', () => {
|
|
it('should show member prompt when remaining quota is 0', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constant(0), // remainingQuota = 0
|
|
(remainingQuota) => {
|
|
return getUnlockActionResult(remainingQuota) === 'show_member_prompt'
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should show member prompt when remaining quota is negative', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: -1000, max: -1 }), // negative quota
|
|
(remainingQuota) => {
|
|
return getUnlockActionResult(remainingQuota) === 'show_member_prompt'
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should proceed with unlock when remaining quota is positive', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 1, max: 1000 }), // positive quota
|
|
(remainingQuota) => {
|
|
return getUnlockActionResult(remainingQuota) === 'proceed_unlock'
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should show unlock button when quota is positive', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 1, max: 1000 }), // positive quota
|
|
(remainingQuota) => {
|
|
return shouldShowUnlockButton(remainingQuota) === true
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should not show unlock button when quota is zero or negative', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: -1000, max: 0 }), // zero or negative quota
|
|
(remainingQuota) => {
|
|
return shouldShowUnlockButton(remainingQuota) === false
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should show member prompt when quota is zero or negative', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: -1000, max: 0 }), // zero or negative quota
|
|
(remainingQuota) => {
|
|
return shouldShowMemberPrompt(remainingQuota) === true
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should not show member prompt when quota is positive', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 1, max: 1000 }), // positive quota
|
|
(remainingQuota) => {
|
|
return shouldShowMemberPrompt(remainingQuota) === false
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Quota calculation after unlock
|
|
*/
|
|
describe('Quota Calculation After Unlock', () => {
|
|
it('should decrease quota by 1 after successful unlock', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 1, max: 1000 }), // positive quota
|
|
(currentQuota) => {
|
|
const newQuota = calculateRemainingQuotaAfterUnlock(currentQuota)
|
|
return newQuota === currentQuota - 1
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should never result in negative quota', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: -1000, max: 1000 }), // any quota
|
|
(currentQuota) => {
|
|
const newQuota = calculateRemainingQuotaAfterUnlock(currentQuota)
|
|
return newQuota >= 0
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should return 0 when current quota is 0 or negative', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: -1000, max: 0 }), // zero or negative quota
|
|
(currentQuota) => {
|
|
const newQuota = calculateRemainingQuotaAfterUnlock(currentQuota)
|
|
return newQuota === 0
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Combined property: Unlock flow consistency
|
|
*/
|
|
describe('Unlock Flow Consistency', () => {
|
|
it('should have consistent button display and action result', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: -100, max: 100 }), // any quota
|
|
(remainingQuota) => {
|
|
const showUnlockBtn = shouldShowUnlockButton(remainingQuota)
|
|
const showMemberPrompt = shouldShowMemberPrompt(remainingQuota)
|
|
const actionResult = getUnlockActionResult(remainingQuota)
|
|
|
|
// Unlock button and member prompt should be mutually exclusive
|
|
if (showUnlockBtn) {
|
|
return !showMemberPrompt && actionResult === 'proceed_unlock'
|
|
} else {
|
|
return showMemberPrompt && actionResult === 'show_member_prompt'
|
|
}
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
})
|