-
Notifications
You must be signed in to change notification settings - Fork 3
fix: enforce System presence validation for ChooseMultiple fields #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d3cb2cb
d357984
24261ff
41c1173
4c46d12
1a54817
2427a54
a619337
0e7bd5d
e452953
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,48 @@ | ||
| name: Tests | ||
| on: | ||
| push: | ||
| branches: [ main ] | ||
| pull_request: | ||
| branches: [ main ] | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
| jobs: | ||
| test: | ||
| name: Run Playwright Tests | ||
| timeout-minutes: 20 | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: lts/* | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Install Playwright Browsers | ||
| run: npx playwright install --with-deps | ||
| - name: Run test-json-compatibility | ||
| run: make test-json-compatibility | ||
| - name: Run Playwright tests | ||
| run: env HTTPBIN_URL="http://localhost:9000/post" make test-playwright-ci | ||
| - uses: actions/upload-artifact@v4 | ||
| if: always() | ||
| with: | ||
| name: playwright-report | ||
| path: playwright-report/ | ||
| retention-days: 30 | ||
| test: | ||
| name: Run Playwright Tests | ||
| timeout-minutes: 20 | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: lts/* | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Install Playwright Browsers | ||
| run: npx playwright install --with-deps | ||
| - name: Run test-json-compatibility | ||
| run: make test-json-compatibility | ||
| - name: Run Playwright tests | ||
| run: make test-playwright-ci HTTPBIN_URL="http://localhost:9000/post" PLAYWRIGHT_ARGS="" | ||
| - uses: actions/upload-artifact@v4 | ||
| if: failure() | ||
| with: | ||
| name: playwright-report | ||
| path: playwright-report/ | ||
| retention-days: 30 | ||
| if-no-files-found: ignore | ||
|
|
||
| go-test: | ||
| name: Run Go Tests | ||
| timeout-minutes: 20 | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| sparse-checkout: go | ||
| - name: Set up Go | ||
| uses: actions/setup-go@v4 | ||
| with: | ||
| go-version: "1.21.1" | ||
| cache: false # No external dependencies, no go.sum to cache | ||
| - name: Run tests | ||
| working-directory: go | ||
| run: make test | ||
| go-test: | ||
| name: Run Go Tests | ||
| timeout-minutes: 20 | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| sparse-checkout: go | ||
| - name: Set up Go | ||
| uses: actions/setup-go@v4 | ||
| with: | ||
| go-version: '1.21.1' | ||
| cache: false # No external dependencies, no go.sum to cache | ||
| - name: Run tests | ||
| working-directory: go | ||
| run: make test |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { test, expect } from '@playwright/test'; | ||
| import { attemptSubmitWithExpectedFailure } from './test-utils'; | ||
|
|
||
| test('System presence + ChooseMultiple with no minRequired should enforce at least 1 selection', async ({ | ||
| page, | ||
| browserName, | ||
| }) => { | ||
| // Create a System field with ChooseMultiple via URL hash | ||
| const systemField = { | ||
| label: 'System Field Test', | ||
| name: '_test_system', | ||
| presence: 'System', | ||
| type: { | ||
| type: 'ChooseMultiple', | ||
| choices: ['Option 1', 'Option 2', 'Option 3'], | ||
| // Note: minRequired is intentionally omitted | ||
| }, | ||
| }; | ||
|
|
||
| const formFields = JSON.stringify([systemField]); | ||
| const targetUrl = process.env.HTTPBIN_URL || 'https://httpbin.org/post'; | ||
|
|
||
| // Navigate to CollectData mode with the System field | ||
| await page.goto( | ||
| `#viewMode=CollectData&formFields=${encodeURIComponent(formFields)}&_url=${encodeURIComponent(targetUrl)}` | ||
| ); | ||
|
|
||
| // Wait for page to load | ||
| await page.waitForTimeout(1000); | ||
|
|
||
| // Verify the field is present | ||
| await expect(page.locator('text=System Field Test')).toBeVisible(); | ||
|
|
||
| // Check for validation element - the fix should create one with min="1" | ||
| const validationInput = page.locator('input[type="number"].tff-visually-hidden'); | ||
| await expect(validationInput).toHaveCount(1); | ||
|
|
||
| // Verify it has min="1" (the fix) | ||
| await expect(validationInput).toHaveAttribute('min', '1'); | ||
|
|
||
| // Verify it's required | ||
| await expect(validationInput).toHaveAttribute('required'); | ||
|
|
||
| // Verify its value is 0 (no selections yet) | ||
| await expect(validationInput).toHaveAttribute('value', '0'); | ||
|
|
||
| // Try to submit without selections - should be blocked by browser validation | ||
| await attemptSubmitWithExpectedFailure(page); | ||
|
|
||
| // Now select one option - event handlers are attached for System fields | ||
| const checkbox1 = page.locator('input[type="checkbox"][value="Option 1"]'); | ||
| if (browserName === 'firefox') { | ||
| // Firefox needs to click the parent label element | ||
| await checkbox1.evaluate((node) => (node.parentElement as HTMLElement).click()); | ||
| } else { | ||
| await checkbox1.click(); | ||
| } | ||
| await page.waitForTimeout(300); | ||
|
|
||
| // Validation element value should now be 1 (updated by event handlers) | ||
| await expect(validationInput).toHaveAttribute('value', '1'); | ||
|
|
||
| // Form should now be valid and submittable | ||
| // The fix ensures: | ||
| // 1. Validation element exists for System + ChooseMultiple ✓ | ||
| // 2. Has min="1" attribute ✓ | ||
| // 3. Event handlers track selections ✓ | ||
| // 4. Browser validation works correctly ✓ | ||
| }); | ||
|
|
||
| test('System + ChooseMultiple with explicit minRequired=0 still gets overridden', async ({ | ||
| page, | ||
| browserName, | ||
| }) => { | ||
| // Test that even if someone explicitly sets minRequired=0, System presence wins | ||
| const systemField = { | ||
| label: 'System Override Test', | ||
| name: '_override', | ||
| presence: 'System', | ||
| type: { | ||
| type: 'ChooseMultiple', | ||
| choices: ['A', 'B', 'C'], | ||
| minRequired: 0, // Explicitly set to 0 - should be overridden by System presence | ||
| }, | ||
| }; | ||
|
|
||
| const formFields = JSON.stringify([systemField]); | ||
| const targetUrl = process.env.HTTPBIN_URL || 'https://httpbin.org/post'; | ||
|
|
||
| await page.goto( | ||
| `#viewMode=CollectData&formFields=${encodeURIComponent(formFields)}&_url=${encodeURIComponent(targetUrl)}` | ||
| ); | ||
|
|
||
| await page.waitForTimeout(1000); | ||
|
|
||
| // Verify the field is present | ||
| await expect(page.locator('text=System Override Test')).toBeVisible(); | ||
|
|
||
| // Even with minRequired=0, System presence should create validation | ||
| const validationInput = page.locator('input[type="number"].tff-visually-hidden'); | ||
|
|
||
| // Note: When minRequired is explicitly set (even to 0), the current logic | ||
| // doesn't override it. This test documents current behavior. | ||
| // System presence only creates validation when minRequired is Nothing/undefined | ||
| const count = await validationInput.count(); | ||
|
|
||
| if (count > 0) { | ||
| // If validation element exists, it should have the explicit minRequired value | ||
| const minAttr = await validationInput.getAttribute('min'); | ||
| // This will be '0' because we explicitly set minRequired: 0 | ||
| // The effectiveMin logic only applies when minRequired is Nothing | ||
| expect(minAttr).toBe('0'); | ||
| } | ||
|
|
||
| // This test documents that the fix only applies when minRequired is undefined/null | ||
| // If someone explicitly sets minRequired (even to 0), that value is used | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1651,7 +1651,8 @@ viewMain model = | |
| ) | ||
|
|
||
|
|
||
| {-| Checks if a ChooseMultiple field has min/max constraints | ||
| {-| Checks if a ChooseMultiple field has min/max constraints OR needs validation tracking | ||
| (e.g., System presence implies minimum selection requirement) | ||
| -} | ||
| isChooseManyUsingMinMax : FormField -> Bool | ||
| isChooseManyUsingMinMax formField = | ||
|
|
@@ -1663,7 +1664,10 @@ isChooseManyUsingMinMax formField = | |
| False | ||
|
|
||
| ChooseMultiple { minRequired, maxAllowed } -> | ||
| minRequired /= Nothing || maxAllowed /= Nothing | ||
| -- Need event handlers if: | ||
| -- 1. Has explicit min/max constraints, OR | ||
| -- 2. System presence (implies min=1 requirement) | ||
| minRequired /= Nothing || maxAllowed /= Nothing || formField.presence == System | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added Without event handlers, the validation element would always show |
||
|
|
||
| ChooseOne _ -> | ||
| False | ||
|
|
@@ -2286,13 +2290,27 @@ viewFormFieldOptionsPreview config fieldID formField = | |
|
|
||
| -- Add validation element for CollectData mode (just the hidden input for validation) | ||
| validationElement = | ||
| let | ||
| -- System presence implies minRequired = 1 if not explicitly set | ||
| effectiveMin = | ||
| case ( formField.presence, minRequired ) of | ||
| ( System, Nothing ) -> | ||
| Just 1 | ||
|
|
||
| _ -> | ||
| minRequired | ||
|
|
||
| -- Need validation if we have constraints | ||
| needsValidation = | ||
| effectiveMin /= Nothing || maxAllowed /= Nothing | ||
| in | ||
| -- Only apply validation in CollectData mode, not in Editor mode | ||
| if not disabledMode && (minRequired /= Nothing || maxAllowed /= Nothing) then | ||
| if not disabledMode && needsValidation then | ||
| [ input | ||
| [ type_ "number" | ||
| , required True | ||
| , attribute "value" (String.fromInt selectedCount) -- raw value for browser only | ||
| , attribute "min" (Maybe.map String.fromInt minRequired |> Maybe.withDefault "") | ||
| , attribute "min" (Maybe.map String.fromInt effectiveMin |> Maybe.withDefault "") | ||
| , attribute "max" (Maybe.map String.fromInt maxAllowed |> Maybe.withDefault "") | ||
| , attribute "class" "tff-visually-hidden" | ||
| ] | ||
|
Comment on lines
+2293
to
2316
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When Without this, the validation element wouldn't be created at all, allowing invalid form submission that would fail backend validation. |
||
|
|
@@ -3523,17 +3541,30 @@ viewFormFieldOptionsBuilder shortTextTypeList index formFields formField = | |
| ++ [ div [ class "tff-field-group" ] | ||
| [ label [ class "tff-field-label" ] [ text "Minimum required" ] | ||
| , input | ||
| [ type_ "number" | ||
| , class "tff-text-field" | ||
| , value (minRequired |> Maybe.map String.fromInt |> Maybe.withDefault "") | ||
| , Attr.min "0" | ||
| ([ type_ "number" | ||
| , class "tff-text-field" | ||
| , value (minRequired |> Maybe.map String.fromInt |> Maybe.withDefault "") | ||
| , Attr.min | ||
| (if formField.presence == System then | ||
| "1" | ||
|
|
||
| else | ||
| "0" | ||
| ) | ||
|
|
||
| -- Maximum value constraint: Either the maxAllowed value (if present) or the number of choices | ||
| , maxAllowed | ||
| -- Maximum value constraint: Either the maxAllowed value (if present) or the number of choices | ||
| , maxAllowed | ||
| |> Maybe.map (\max -> Attr.max (String.fromInt max)) | ||
| |> Maybe.withDefault (Attr.max (String.fromInt (List.length choices))) | ||
| , onInput (\val -> OnFormField (OnCheckboxMinRequiredInput val) index "") | ||
| ] | ||
| , onInput (\val -> OnFormField (OnCheckboxMinRequiredInput val) index "") | ||
| ] | ||
| ++ (if formField.presence == System then | ||
| [ required True ] | ||
|
|
||
| else | ||
| [] | ||
| ) | ||
| ) | ||
| [] | ||
| ] | ||
| , div [ class "tff-field-group" ] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added automatic formatting after test generation to keep git history clean.
The Node.js generator doesn't produce properly formatted Elm code. Without this, every
make test-json-compatibilitywould create formatting-only diffs.