diff --git a/admin/src/federated-element.test.tsx b/admin/src/federated-element.test.tsx
index a18976c..de278a2 100644
--- a/admin/src/federated-element.test.tsx
+++ b/admin/src/federated-element.test.tsx
@@ -363,6 +363,16 @@ describe('PlaytestCreatePage', () => {
expect(steamRadio).toBeChecked()
})
+ it('requires an initial code quantity when AGS Campaign is selected', async () => {
+ renderAt('/new')
+ const user = userEvent.setup()
+ await gotoDistributionStep(user)
+ await user.click(screen.getByRole('radio', { name: /AGS Campaign/i }))
+ // leave the quantity empty and try to advance
+ await user.click(screen.getByRole('button', { name: /^next$/i }))
+ expect(await screen.findByText('Must be between 1 and 50000')).toBeInTheDocument()
+ })
+
it('shows all the PRD-required fields across the create wizard', async () => {
renderAt('/new')
const user = userEvent.setup()
@@ -414,6 +424,19 @@ describe('PlaytestCreatePage', () => {
expect(mutate).not.toHaveBeenCalled()
})
+ it('shows the short range message when the auto-approve limit is left empty', async () => {
+ const mutate = vi.fn()
+ mockCreateMutation.mockReturnValue({ mutate, isPending: false, isError: false, error: null })
+ renderAt('/new')
+ const user = userEvent.setup()
+ await gotoAccessStep(user)
+ await user.click(screen.getByRole('radio', { name: /auto-approve/i }))
+ // leave the limit empty, then submit
+ await user.click(screen.getByRole('button', { name: /create playtest/i }))
+ expect(await screen.findByText('Must be between 1 and 100000')).toBeInTheDocument()
+ expect(mutate).not.toHaveBeenCalled()
+ })
+
it('offers the ADT distribution radio (M5.B)', async () => {
renderAt('/new')
const user = userEvent.setup()
diff --git a/admin/src/federated-element.tsx b/admin/src/federated-element.tsx
index f58f2f3..17f9333 100644
--- a/admin/src/federated-element.tsx
+++ b/admin/src/federated-element.tsx
@@ -577,6 +577,7 @@ const AUTO_APPROVE_LIMIT_MIN = 1
const AUTO_APPROVE_LIMIT_MAX = 100000
const AUTO_APPROVE_LIMIT_ERROR =
'auto_approve_limit must be between 1 and 100000 when auto_approve is true'
+const AUTO_APPROVE_LIMIT_EMPTY_ERROR = 'Must be between 1 and 100000'
// Mirrors the errors.md row for CreatePlaytest / EditPlaytest banner URL —
// the backend rejects http with the byte-exact "banner_image_url must be
@@ -598,9 +599,33 @@ const bannerImageUrlRule = {
}
}
+const INITIAL_CODE_QUANTITY_MIN = 1
+const INITIAL_CODE_QUANTITY_MAX = 50000
+const INITIAL_CODE_QUANTITY_ERROR = 'Must be between 1 and 50000'
+
+const initialCodeQuantityRule = ({ getFieldValue }: { getFieldValue: (name: string) => unknown }) => ({
+ validator(_: unknown, value: unknown) {
+ if (getFieldValue('distributionModel') !== DistributionModel.AGS_CAMPAIGN) return Promise.resolve()
+ if (
+ value == null ||
+ value === '' ||
+ typeof value !== 'number' ||
+ !Number.isInteger(value) ||
+ value < INITIAL_CODE_QUANTITY_MIN ||
+ value > INITIAL_CODE_QUANTITY_MAX
+ ) {
+ return Promise.reject(new Error(INITIAL_CODE_QUANTITY_ERROR))
+ }
+ return Promise.resolve()
+ }
+})
+
const autoApproveLimitRule = ({ getFieldValue }: { getFieldValue: (name: string) => unknown }) => ({
validator(_: unknown, value: unknown) {
if (!getFieldValue('autoApprove')) return Promise.resolve()
+ if (value == null || value === '') {
+ return Promise.reject(new Error(AUTO_APPROVE_LIMIT_EMPTY_ERROR))
+ }
if (typeof value !== 'number' || !Number.isInteger(value) || value < AUTO_APPROVE_LIMIT_MIN || value > AUTO_APPROVE_LIMIT_MAX) {
return Promise.reject(new Error(AUTO_APPROVE_LIMIT_ERROR))
}
@@ -1072,13 +1097,21 @@ function PlaytestCreatePage() {
layout="vertical"
onFinish={handleSubmit}
scrollToFirstError={{ behavior: 'smooth', block: 'center' }}
- style={{ marginTop: 16, flex: 1 }}
+ style={{ marginTop: 16, flex: 1, display: 'flex', flexDirection: 'column' }}
initialValues={{
platforms: [],
ndaRequired: false,
distributionModel: DistributionModel.STEAM_KEYS,
autoApprove: false
}}>
+ {createMutation.isError && (
+