Skip to content

Forms: Allow submitting form previews as test responses#48057

Merged
enejb merged 19 commits intotrunkfrom
add/submit-preview-forms
Apr 18, 2026
Merged

Forms: Allow submitting form previews as test responses#48057
enejb merged 19 commits intotrunkfrom
add/submit-preview-forms

Conversation

@enejb
Copy link
Copy Markdown
Member

@enejb enejb commented Apr 11, 2026

Fixes FORMS-638

Proposed changes

  • Form preview submissions are now allowed. The PHP and JS guards that blocked submission from ?jetpack_form_preview=… previews have been replaced with a form-id-scoped nonce verification, so editors can submit the form to verify the entire pipeline (validation, success state, email delivery).
  • Test feedback is stored on Feedback_Source. When the submission comes from preview, the feedback is flagged via a new is_test field on Feedback_Source that travels inside the serialized v3 post content. Storage uses the normal publish post status — no new post status, no new post meta.
  • Akismet is bypassed for test submissions and the unread counter is not bumped (test responses are created already-read).
  • Notification email is annotated. Test submissions always send the email (gated by a new jetpack_forms_send_test_feedback_email filter), the subject is prefixed with [TEST] (filterable via jetpack_forms_test_subject_prefix), and the email body gets a prominent banner. The "Mark as spam" footer link is dropped for test entries.
  • Dashboard surfaces test responses.
    • List, From column: small Test badge (intent="informational") rendered before the respondent name.
    • List, Source column: linked Form Preview badge in place of the source URL.
    • Detail panel: Test badge as the first line above the respondent block, and the Source row renders a Form Preview link to the regenerated preview URL.
  • REST endpoint exposes is_test and preview_url. Schema gets two new read-only properties; prepare_item_for_response() populates them. preview_url is generated via Form_Preview::generate_preview_url() against the feedback's parent form id, only for test responses.
  • CSV export auto-handles test responses.
    • No selection → test responses are excluded automatically; the export modal shows an info notice at the bottom.
    • Selection contains test rows → the modal shows a warning notice at the top stating they will be included.
    • No checkbox, no opt-in clicks. The "include" decision is derived purely from whether the user provided an explicit selection.

Related product discussion/links

  • N/A

Does this pull request change what data or activity we track or use?

No. The new is_test flag lives in the existing serialized feedback content; no new tables, no new tracks events, no new tracking surfaces. Notification emails go to the same recipients they always have.

Testing instructions

  1. Open the editor for any page that contains a Contact Form block (or open a jetpack_form directly).
  2. Click the form's Preview action — this opens a server-side preview at ?jetpack_form_preview=<id>&preview_nonce=…. Confirm the banner now reads "This is a preview. Submissions are saved as test responses."
  3. Fill out the form and submit it.
    • Expect the standard confirmation screen (no "submissions are disabled" error).
    • Expect a notification email to land in the form owner's inbox with a [TEST] subject prefix and an amber Test submission from form preview banner at the top of the body.
  4. Open the responses dashboard and confirm:
    • The new row appears in the Inbox.
    • The From column shows a small blue Test badge before the respondent name.
    • The Source column shows a linked Form Preview badge instead of an external URL.
    • Clicking the row's detail panel shows a Test badge above the gravatar/name block, and the Source: row reads Form Preview linked to the preview URL.
  5. Open the Export responses modal with no rows selected → expect an info notice at the bottom: "Test responses from form preview are excluded from this export…". Download the CSV → confirm the test row is absent.
  6. In the responses list, tick the test row's checkbox, then re-open the export modal → expect a warning notice at the top: "Your selection includes 1 test response from form preview…". Download the CSV → confirm the test row is now present.
  7. From the dashboard, mark the test entry as spam → it should move to the Spam view like any other spam entry.
  8. Adversarial: open a fresh preview URL while logged out (or DevTools-tamper the hidden jetpack_form_preview_nonce) and submit → expect the submission to be rejected.
  9. Run the package test suites:
    cd projects/packages/forms
    composer test-php
    pnpm test
    

Dashboard:
Screenshot 2026-04-13 at 4 15 16 PM

Email:
Screenshot 2026-04-13 at 4 15 25 PM

Export modal (default)
Screenshot 2026-04-13 at 4 15 06 PM

Export modal (with test responses selected)
Screenshot 2026-04-13 at 4 14 59 PM

@enejb enejb added [Feature] Forms [Status] Needs Review This PR is ready for review. labels Apr 11, 2026
@enejb enejb self-assigned this Apr 11, 2026
@enejb enejb added [Feature] Forms [Status] Needs Review This PR is ready for review. labels Apr 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 11, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the add/submit-preview-forms branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/submit-preview-forms

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@jp-launch-control
Copy link
Copy Markdown

jp-launch-control Bot commented Apr 11, 2026

Code Coverage Summary

Coverage changed in 13 files. Only the first 5 are listed here.

File Coverage Δ% Δ Uncovered
projects/packages/forms/src/contact-form/class-contact-form-endpoint.php 921/1099 (83.80%) -0.54% 15 💔
projects/packages/forms/src/contact-form/class-feedback-email-renderer.php 311/351 (88.60%) -3.49% 14 💔
projects/packages/forms/src/dashboard/components/inspector/response-meta/index.tsx 0/25 (0.00%) 0.00% 9 💔
projects/packages/forms/src/dashboard/inbox/stage/index.js 0/202 (0.00%) 0.00% 8 💔
projects/packages/forms/src/dashboard/components/export-responses/modal.tsx 0/13 (0.00%) 0.00% 6 💔

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

Comment on lines +401 to +403
if ( ! current_user_can( 'edit_post', $form_id ) ) {
return false;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if “previewing unpublished posts” type capability would be more suitable here :thinking_face:

@simison
Copy link
Copy Markdown
Member

simison commented Apr 13, 2026

Image

We should think about how to improve the "test" badge:

  • Visually looks clunky, especially in the row.

  • Sidebar is better, though spacing feels off by being inconsistent in every direction: adding bottom padding might help?

  • How does it look with variation without name, without email, or with just IP?

  • Link for a different action (preview form) within within name area which also is a link (to see response) is surprising. It's also not clear that "test" would lead to previewing the form.

For the "preview" link/badge:

image

Let's keep links looking like links rather than badges, and ↗ there to indicate links opening new tabs.

image

I think we could just not have this notice at all — from reading code it looks like it appears even if user doesn't have any test responses?

* @return The badge element.
*/
function FormPreviewBadge( { previewUrl }: { previewUrl?: string | null } ) {
const label = __( 'Form Preview', 'jetpack-forms' );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const label = __( 'Form Preview', 'jetpack-forms' );
const label = __( 'Form preview', 'jetpack-forms' );

We should always use sentence case.

enejb added a commit that referenced this pull request Apr 13, 2026
Follow-ups from review feedback on #48057:

- Replace the hidden-field + nonce injection in form preview rendering
  with a flag baked into the signed JWT. Feedback_Source::get_current()
  now reads Form_Preview::is_preview_mode() at render time and records
  is_test=true on the source, which travels tamper-proof inside the
  JWT to submission. JWTs issued before this change omit the flag and
  continue to behave as regular submissions.
- Remove the now-unused inject_preview_submission_fields() helper,
  verify_preview_submission() helper, and the three PREVIEW_SUBMIT_*
  constants from Form_Preview.
- Drop the test-feedback special case in Feedback::save(); test
  responses are now created as STATUS_UNREAD like any other response
  so they contribute to the unread count.
- Add aria-label="Test response" to the inline Test badge in the
  dashboard From column so screen readers get a complete label.
- Add two PHPUnit round-trip tests covering the JWT backward-compat
  contract: a JWT issued in preview mode marks the decoded form as
  a test submission; a JWT issued outside preview mode (or from a
  cached HTML fragment predating this feature) decodes to a regular
  submission.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ilonagl
Copy link
Copy Markdown
Contributor

ilonagl commented Apr 13, 2026

This is great work, @enejb! So glad you're working on this. 🙌

Screenshot 2026-04-13 at 20 22 52

The badge placement pushes the whole row down, which isn't ideal. Could we use the same white badge as in the dataViews, but place it in the same row (centered) with name and email, just place it on the other side (right)?

Screenshot 2026-04-13 at 20 25 24

I'd place the warning outside the content, and above the content card at the top.

Screenshot 2026-04-13 at 20 26 16

The warning makes it feel like something is wrong, but test data is nothing dangerous. I wonder if we even need to announce/share this?

Screenshot 2026-04-13 at 20 28 55 Screenshot 2026-04-13 at 20 28 43

I feel like the source should still be the actual name of the form.

Screenshot 2026-04-13 at 20 29 45

The badge in the front breaks the flow, let's add at in the end. And also I'd consider the badge to be either blue or white, one of them, because two of them clashes together.

I hope this is helpful. 😅

@ilonagl
Copy link
Copy Markdown
Contributor

ilonagl commented Apr 13, 2026

Screenshot 2026-04-13 at 20 32 29

Maybe it's enough just a simple heading that this is a test data?

@enejb
Copy link
Copy Markdown
Member Author

enejb commented Apr 13, 2026

@ilonagl and @simison thanks for all the feedback.
Will try to get address the feedback soon. If you have any more suggestions please let me know.

enejb and others added 3 commits April 13, 2026 15:25
Form preview now lets you submit the form to test the full submission
flow end to end. Responses created from preview are stored normally in
the inbox but flagged as test responses: clearly badged in the dashboard
list and detail panel, prefixed with [TEST] in the notification email
with a banner, excluded from the default CSV export, and excluded from
exports unless explicitly selected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move CSVExport and GoogleDriveExport components above the selected-test warning and remove the duplicate render. The Google Drive export remains conditionally rendered when enabled and preserves the autoConnect prop. This reordering makes export controls visible before the warning about exporting test responses.
Follow-ups from review feedback on #48057:

- Replace the hidden-field + nonce injection in form preview rendering
  with a flag baked into the signed JWT. Feedback_Source::get_current()
  now reads Form_Preview::is_preview_mode() at render time and records
  is_test=true on the source, which travels tamper-proof inside the
  JWT to submission. JWTs issued before this change omit the flag and
  continue to behave as regular submissions.
- Remove the now-unused inject_preview_submission_fields() helper,
  verify_preview_submission() helper, and the three PREVIEW_SUBMIT_*
  constants from Form_Preview.
- Drop the test-feedback special case in Feedback::save(); test
  responses are now created as STATUS_UNREAD like any other response
  so they contribute to the unread count.
- Add aria-label="Test response" to the inline Test badge in the
  dashboard From column so screen readers get a complete label.
- Add two PHPUnit round-trip tests covering the JWT backward-compat
  contract: a JWT issued in preview mode marks the decoded form as
  a test submission; a JWT issued outside preview mode (or from a
  cached HTML fragment predating this feature) decodes to a regular
  submission.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@enejb enejb force-pushed the add/submit-preview-forms branch from d0486e4 to b7623ff Compare April 13, 2026 22:26
enejb added 8 commits April 13, 2026 15:41
- List view: move Test badge to end of the From row, switch to the
  neutral white badge, and show the form title (linked to preview)
  in the Source column instead of the Form Preview badge.
- Detail panel: drop the standalone banner row, render the Test
  badge inline (right-aligned) next to name/email, and link the
  form title in the Source row.
- Export modal: drop the bottom info notice, move the top notice
  above the content card, and tone it down from warning to info.
- Email: replace the amber test-submission banner with a simple
  heading line above the feedback body.
Revert to surfacing "Form preview" (sentence case) as the Source
label for test feedback in both the list Source column and the
detail panel, linked to the regenerated preview URL when available.
Apply the same "Form preview" Source label + preview-URL link to
the legacy DataViews inbox list so test responses surface
consistently whether or not CFM is enabled.
Bring back the amber notice block at the top of test-submission
emails, but drop the secondary explanatory sentence — just the
"Test submission from form preview" heading now.
Pass the test-submission banner through wrap_message_in_html_tags
as a new optional argument and render it above the white card in
the email template, so the rest of the body still reads like a
normal submission email.
Swap the banner copy from "Test submission from form preview" to
"Test response via form preview" to match the terminology used in
the dashboard.
Override the Source row in the notification email metadata table
so test responses read as "Form preview" (no link), matching the
dashboard treatment, instead of the hosting page URL.
Keep the original top-left alignment for the gravatar/name block in
the single response header. The Test badge keeps itself vertically
centered with alignSelf so test rows still look balanced.
Wrap the test-response notice in a spacer div so it doesn't
butt up against the CSV export card below.
@enejb
Copy link
Copy Markdown
Member Author

enejb commented Apr 13, 2026

Email:
Screenshot 2026-04-13 at 4 15 25 PM

Dashboard:
Screenshot 2026-04-13 at 4 15 16 PM

Export modal (default)
Screenshot 2026-04-13 at 4 15 06 PM

Export modal (with test responses selected)
Screenshot 2026-04-13 at 4 14 59 PM

@simison
Copy link
Copy Markdown
Member

simison commented Apr 14, 2026

It's a bummer that injecting the badge into the name now doesn't allow filtering just tests to easily remove them. Any way around it? E.g. filtrable/sortable but without its own column?

>
<VStack spacing={ 8 }>
{ selectedTestCount > 0 && (
<div style={ { marginBottom: '24px' } }>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the VStack take care of the spacing if Notice would go inside it?

*/
private static function build_test_submission_banner() {
return sprintf(
'<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%%" style="border-collapse: collapse; margin: 0 0 24px 0;">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the modern notice styles here instead of the deprecated component?

enejb added 7 commits April 17, 2026 17:46
Replace the planned separate "Response type" filter with a virtual
"Form preview" option injected at the top of the existing Source filter.
When selected, the source value "form_preview" is translated to is_test=true
on the query. Keeps filtering UX to a single pill and avoids the extra
filter chrome.

Also exposes is_test on the feedback REST collection (GET /feedback) and
stamps test submissions with the _feedback_is_test post meta on insert,
so the filter can scope at the database level without unpacking the
serialized source.
Move the "Test" badge out of a marginLeft:auto span and into a row next
to the respondent name in both the response list and the single-response
inspector. The auto-margin pushed the badge to the cell's far edge, which
on narrow widths could overflow into the adjacent Date column.
Drop the 4px left-border accent in favor of the @wordpress/ui Notice
warning palette: 1px amber border, 8px radius, warm amber fill
(#fff7e0 / #d0b381), and 13px/20px body copy in #2e1900. Punctuate the
body copy ("Test response via form preview.") and add mobile-only side
margin so the banner aligns with the rest of the email content on
narrow viewports.

Also fold the export modal's test-response Notice into the same VStack
as the export options instead of wrapping it in a bespoke marginBottom
div, so the 24px spacing comes from the VStack like every other child.
Replace the legacy @wordpress/components Notice (status="info",
isDismissible) with the @wordpress/ui Notice.Root + Notice.Description
(intent="info") so the test-response warning in the Export responses
modal matches the rest of the new WPDS-based surfaces.
The phpunit.11.xml.dist config sets failOnDeprecation="true", which on
PHP 8.5 treats the ReflectionProperty::setAccessible() deprecation as a
fatal signal — making the packages/forms test job exit non-zero even
when every assertion passes.

Pass --do-not-fail-on-deprecation to phpunit-select-config so the run
succeeds on deprecation notices, and --display-deprecations so they
still surface in the CI log for visibility.
Replace the composer-level --do-not-fail-on-deprecation / --display-deprecations
workaround with a version-guarded call at the source. PHP 8.1 made
setAccessible() a no-op for all reflection objects and PHP 8.5 deprecates
calling it on ReflectionProperty, so on 8.1+ the package-supported branch
now skips the call entirely.

The 7.2–8.0 codepath still invokes setAccessible() so the test keeps
working against the package's minimum PHP.
The guard is purely a test-only change (never ships to users), so it
doesn't need a changelog entry.
@enejb enejb merged commit 54cb1b0 into trunk Apr 18, 2026
70 of 71 checks passed
@enejb enejb deleted the add/submit-preview-forms branch April 18, 2026 15:06
@github-actions github-actions Bot added [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. and removed [Status] Needs Review This PR is ready for review. labels Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants