Skip to content

feat(jobs): Add submit_easy_apply tool - #1427

Open
shbernal wants to merge 27 commits into
stickerdaniel:mainfrom
shbernal:feature/easy-apply-submit
Open

shbernal wants to merge 27 commits into
stickerdaniel:mainfrom
shbernal:feature/easy-apply-submit

Conversation

@shbernal

@shbernal shbernal commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1426

Summary

Adds submit_easy_apply(job_id, answers, follow_company=false, confirm_submit=false), which answers a posting's Easy Apply form by the labels get_easy_apply_form returned and sends it only when asked to.

  • It returns {url, apply: {type}, submitted, form?: {steps, complete}, follow?, confirmation?}. form is shaped as get_easy_apply_form's, with the fields as the run left them. Any posting that is not Easy Apply answers with its type alone and is never opened.
  • answers maps a field's label to its answer: text for a typed field or a select, an option's label for a radio, a list of labels for a checkbox group, true or false for a lone checkbox, and a path on the server's machine for a resume upload. Nothing is inferred or defaulted. A field no answer names keeps LinkedIn's prefill.
  • To reach a control, the step program now also returns each field's control ids. _public_step drops them, so get_easy_apply_form's output is unchanged.
  • Every answer of a step is resolved before the step is touched. A refusal names the field, its type and its options. It is raised for an unknown label, two fields sharing a label, a missing option, an answer longer than max_length, a file that is not there, a radio or checkbox group the read cut at 50 options, and an empty typeahead nothing names. A required field still empty stops the run before Next, including one an answer emptied, such as "" or false. An answer no step asked for refuses after the last step, before Submit. These raise the new InvalidAnswerError, which maps to invalid_argument without issue diagnostics, like InvalidReferenceError.
  • A select the read cut at 50 options (a country list, for example) is checked against its whole list, which the step read keeps aside and never returns.
  • A typeahead is typed one key at a time. The suggestion whose text equals the answer, after NFC and whitespace normalisation only, is clicked. The pick counts once the field holds the answer and the list has closed. No suggestion matching within 8 seconds, or a click LinkedIn does not take within 3, clears the field and refuses. The no-match refusal quotes up to 20 of LinkedIn's suggestions. A prefill that already is the answer is not retyped. Picks run before the step's other edits, so a failed pick leaves the rest of the dialog as LinkedIn prefilled it. Keyboard selection is not used, because it picks by position, and Enter with no list open advances the step.
  • After an upload, the walk waits up to 30 seconds for the form to name the file back, matched as a whole name, and refuses if it never does. A card LinkedIn already held under the same name cannot be told apart from the new one. Waiting for the step to change is not enough: the upload empties the resume list first, and the empty list holds still.
  • Steps are told apart by heading and progress, not by the read's signature, because an upload re-renders the same step. A step Next does not move off ends the run with complete: false, with only the errors the refused click added on its fields, and nothing sent. A step that moved is never judged on its text, so a hint on a newly reached step does not stop the run, and the returned steps drop hints as get_easy_apply_form's do. So does the eight-step cap.
  • Submit (data-live-test-easy-apply-submit-button) is clicked only with confirm_submit, and only after every step was reached with its required fields held. A dialog still open 15 seconds after the click raises; it is not reported as sent. confirmation is LinkedIn's modal text or, where no modal renders, the posting's applied line, polled for 5 seconds. It is absent when neither appears or the read fails after the dialog closed, and that says nothing about whether the application went out; submitted does. The applied line is read above the description heading through the same fragment APPLY_SIGNALS_JS uses, now shared.
  • The follow-the-company checkbox, which LinkedIn ships checked, is unchecked unless follow_company is true. It is tried up to three times: check/uncheck on the input by id or as the footer's checkbox, then a click on its label when the input cannot be clicked. follow reports the state it ended in. A box that will not change does not fail the run.
  • Without confirm_submit, the form is filled, read back and the draft discarded. On every exit that did not submit, a cancelled call or a tool timeout included, the dialog is closed with layer 1's shielded, bounded discard before the error leaves. A draft that will not close is section_errors.form with error_type: easy_apply_draft_left, raised as EasyApplyDraftLeftError as the read does. Any other walk failure (the dialog did not open, Submit left it open) is section_errors.form with issue diagnostics and submitted: false.
  • A posting whose Easy Apply is a <button> with no href is refused before anything is clicked: section_errors.form with the message "This posting's Easy Apply opens LinkedIn's redesigned dialog, which the form tools cannot read yet. Nothing was opened." The guard sits in the shared opener, so get_easy_apply_form now answers the same immediately instead of after the five-second click timeout described in feat(jobs): Add get_easy_apply_form tool #1343.
  • The tool is readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, and uses tool_errors like the other tools. The docstring says that every run writes to the account, submitted or not, because LinkedIn saves Easy Apply answers and prefills them into later applications. The plugin skill lists it as a write action that needs the user's authorization for the posting and the answers.
  • AGENTS.md gains a scraping rule: the Easy Apply dialog is not in the document. docs/easy-apply-markup.md records the live typeahead markup.

Documented in AGENTS.md, CONTRIBUTING.md, README.md, docs/docker-hub.md, manifest.json, the plugin skill and docs/easy-apply-markup.md.

What this PR does not cover

This PR answers only the dialog that the posting's /jobs/view/<id>/apply/ anchor opens. Postings measured since 2026-09-25 render Easy Apply as a <button> with no href (#1248, #1250), which opens a redesigned dialog: a native <dialog> in the document whose buttons carry none of these data attributes. Both form tools refuse those postings before opening anything, as above. A follow-up PR will add reading and answering the redesigned dialog once this one merges.

The walk clicks only Next (data-easy-apply-next-button) and Submit. A form with a separate review page before Submit has not been seen live or tested.

Live evidence

From the anchor's dialog, recorded in the commits and in docs/easy-apply-markup.md. All of it is from 2026-09-23:

  • Typeahead markup, measured with the dialog opened, a city typed and the draft discarded; no step advanced. The combobox, the listbox rendered beside it inside the shadow root, aria-owns and aria-controls naming it, role="option" suggestions, an empty aria-activedescendant, and no control carrying the entity. Suggestions came within 300 ms. Clicking a suggestion was not tried live; the pick is tested only against the synthetic dialog.
  • The first application sent through a fork build landed with confirmation: "": no modal rendered, while the posting showed its applied line and get_job_details read it as applied. The applied-line fallback came from that run. It has not been observed live since it was added.
  • On a live application, the resume upload emptied the resume list while in flight, and the empty list held still. The follow checkbox came back checked on two live runs: once with no id on its input, and with the input styled invisible behind its label. The wait for the file name and the label click came from those runs and have not been observed live since.
  • A country select on a live form ran to 250 options.

The live applications were single-step forms holding Submit from the start. A multi-step submission has not been run live. Whether a run that does not submit saves its answers to the account is listed as not measured in docs/easy-apply-markup.md; the docstring takes the cautious reading. The button-shaped refusal follows from the code and was not observed live. Nothing was rerun on this exact commit.

Verification

  • tests/test_easy_apply_submit_dom.py (browser_dom) runs the whole walk in headless chromium against a synthetic dialog: an open shadow root, LinkedIn's data attributes, required fields validated on Next, a hidden file input behind an upload button, a resume list that grows a card for the file, a typeahead whose suggestions render in the same root and bind an entity only when clicked, and Submit beside the follow checkbox in the footer. It logs every click. The tests cover every control type set from its label; an uploaded file landing, its re-rendered step not read as a refusal, a list torn down by the upload waited out, and a file never shown back not submitted; the draft discarded without the flag and after an AnyIO deadline mid-walk; a required field nothing answers refusing before any click; Submit clicked once with every answer; the follow checkbox unchecked by default, left checked when asked, and unchecked through its label when the input is hidden; a one-step form submitting from its footer; a send with no modal confirmed by the posting's line, and a matching line in the description not taken for it; and the typeahead picked by its text, refused with the suggestions quoted, a refused pick leaving the step as prefilled, a prefill replaced or kept, an empty unnamed typeahead refused, typed text without a pick not answering the step, and a suggestion LinkedIn does not take refused.
  • tests/test_easy_apply_form_dom.py adds a button-shaped posting: the read refuses with nothing clicked.
  • tests/linkedin/test_job_pages.py: TestAnswerEasyApplyStep covers how each answer resolves into edits and every refusal above. TestSubmitEasyApply covers the walk on a mocked page: only the flag clicks Submit, the confirmation sources and their order, a failed confirmation read that keeps the send, a hint on a newly reached step that does not stop the run, a cut select checked against its whole list, a refused or stalled step, the upload waits, the follow checkbox fallbacks, unused answers, the step cap, a dialog still open after Submit, and a non-Easy Apply posting. A failed walk that leaves a draft raises EasyApplyDraftLeftError.
  • tests/linkedin/test_jobs.py covers the workflow: a sent application with and without a confirmation, a rehearsal, a refused answer reaching the caller, a failed walk as a section error with nothing sent, a draft left beside the form or in place of it, and any other posting type. tests/test_error_handler.py and tests/test_error_codes.py cover InvalidAnswerError as invalid_argument without diagnostics, and tests/test_tools.py adds the tool to the every-tool tests.
  • The tool contract, the facade contract, the policy traces (a new submit-easy-apply scenario) and the architecture reference were regenerated. ruff, ty, the architecture check and the policy-trace check pass.
  • Full suite in fork CI on this PR's head (a25dbb61), run from the branch stacked on it: every job passed, the test job included.

Synthetic prompt

Add a submit_easy_apply(job_id, answers, follow_company=false, confirm_submit=false) tool beside get_easy_apply_form, for the dialog the posting's /apply/ anchor opens. Answers are keyed by the exact labels the read returned: text, an option label, a list for a checkbox group, a bool for a lone checkbox, a file path for an upload. Have the step program carry each field's control ids, dropped from the public step. Resolve a whole step's answers before touching it and refuse, with a new InvalidAnswerError mapped to invalid_argument without diagnostics, on unknown or duplicate labels, missing options, overlong answers, missing files, cut choice lists, required fields left empty and answers no step used; leave a cut select to the browser. Answer a typeahead by typing key by key, clicking the suggestion whose normalised text equals the answer, and confirming by value and the list closing; otherwise clear it and refuse quoting the suggestions. Run picks first. After an upload, wait until the form names the file. Tell steps apart by heading and progress. Click Submit by its data attribute only with confirm_submit after every step; raise if the dialog stays open, and report LinkedIn's modal or the posting's applied line as confirmation, omitting it when empty. Uncheck the follow checkbox unless asked, falling back to its label, and report its state. Discard the draft on every exit that did not submit, cancellation included, raising EasyApplyDraftLeftError when it will not close. Refuse a button-shaped Easy Apply before opening anything, in both form tools. Annotate it destructive, use tool_errors, document it, record the live typeahead markup in docs/easy-apply-markup.md, add browser-DOM, page, workflow, error and tool tests, and regenerate the tool contract, facade contract, policy traces and architecture reference.

Screenshots

README.md, tool table:

Before After
Before After

docs/docker-hub.md, features:

Before After
Before After

Implemented with Claude Opus 5.5 (high) in Claude Code via tmux.

Note

Add submit_easy_apply tool for answering and sending Easy Apply forms

  • Adds a submit_easy_apply MCP tool registered in job.py, exposed through LinkedInExtractor and JobReader. It takes a job ID, a label-to-answer mapping, a follow_company flag, and a confirm_submit flag; without confirmation it fills the form, discards the draft, and does not send.
  • Implements the form walk in job_pages.py: it opens the Easy Apply dialog inside the shadow root of div#interop-outlet, plans answers per control type (text, select, radio, checkbox, textarea, file upload, typeahead suggestions), and clicks Next/Submit. Refusal messages live in job_policy.py.
  • Adds InvalidAnswerError in exceptions.py and classifies it as INVALID_ARGUMENT in error_handler.py, passing refusal messages to the caller without diagnostics.
  • Covers the flow with Playwright DOM tests in test_easy_apply_submit_dom.py, unit tests across the reader, facade, and tool layers, and updates contracts, fixtures, docs, and the manifest.
  • Risk: submit_easy_apply is annotated destructive and non-idempotent — confirm_submit sends an application that cannot be withdrawn, and answers are saved to the account. The redesigned Easy Apply button is refused before opening (EASY_APPLY_BUTTON_UNREAD), and a failed walk that cannot close the draft raises EasyApplyDraftLeftError.

Macroscope summarized d04567d.

@github-actions github-actions Bot added the enhancement New feature or request label Nov 2, 2026
shbernal added a commit to shbernal/linkedin-mcp-server that referenced this pull request Nov 2, 2026
@shbernal
shbernal marked this pull request as ready for review November 2, 2026 15:49
@greptile-apps

greptile-apps Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; the blank-selection finding is fixed and no new actionable issues were found.

Summary

Adds submit_easy_apply to fill Easy Apply forms and send them only with explicit confirmation.

  • Callers can fill Easy Apply forms and send them only when confirmed.
  • Easy Apply readers refuse button-shaped postings before opening them.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Read posting type] --> B{Easy Apply?}
  B -->|No| C[Return posting type]
  B -->|Yes| D[Open supported dialog]
  D --> E[Resolve and fill step answers]
  E --> F[Read fields back and check required answers]
  F --> G{More steps?}
  G -->|Yes| E
  G -->|No| H{Explicit confirmation?}
  H -->|No| I[Discard draft]
  H -->|Yes| J[Click Submit and wait for form to close]
  J --> K[Read optional confirmation]
Loading

Reviews (4) · Last reviewed commit: "fix(jobs): Read a select left on its pla..." · Reviewed by Greptile

The step program threw its controls away once it had read them, so a form
could be described and not answered. It now carries each field's control
ids, which `_public_step` drops again, and an answer keyed by the label the
read returned resolves into the edits that leave the field holding it.

Every answer is resolved before the first one is carried out. LinkedIn saves
Easy Apply answers to the account and prefills them into the next
application, so a step half filled before a refusal is not undone by
discarding the draft.

Nothing is inferred. A label the form does not ask for, two fields wearing
one label, an option that does not exist, an answer longer than the field
holds and a missing resume each refuse, naming the field and what it takes.
A typeahead refuses outright: its answer is a suggestion picked while
typing, and a wrong one is what LinkedIn would save.
Opens the dialog, fills each step from the answers, clicks Next, and ends on
the last step. Submit is clicked only when the caller asked for it and only
once every step was reached holding an answer for each of its required
fields. Without the flag the same walk fills the form, reads back what it
would send and discards the draft, which is the rehearsal before a real
application.

Advancement is decided on the step's own heading and progress rather than on
the signature the read walk compares. Answering a file field re-renders the
step around the card for the uploaded file: same step, new controls. The read
walk can compare signatures because it types nothing.

The follow-the-company checkbox sits in the footer the step program skips, so
it gets its own parameter and is left unchecked unless asked for; LinkedIn
ships it checked. A dialog still holding the form after Submit is raised
rather than reported, so nothing downstream records an application on the
strength of a click.
Puts the answering walk on the MCP beside get_easy_apply_form, which is where
its answers come from: they are keyed by the exact field labels that read
returned. The result carries the same form, as the walk left it, beside what
it did.

Annotated destructive rather than idempotent. A submitted application cannot
be taken back, and confirm_submit is the only thing that sends one, so the
client prompts for it.

The docstring is what an agent reads before calling, so it says the part that
is not visible from the signature: every run writes to the account, submitted
or not, because LinkedIn saves Easy Apply answers and prefills them into the
next application.
The unit suite mocks page.evaluate, so nothing it covers has ever set a real
control or run the programs the walk reads the dialog with. This runs the
whole walk against a synthetic dialog in headless chromium: an open shadow
root, LinkedIn's data attributes, required fields validated on Next, a file
input behind an upload button, a resume list that grows a card for the
uploaded file, and a footer holding the follow checkbox beside Submit.

It covers the one shape the next live application has, a single step holding
Submit in the footer slot from the start, and the claims worth having before
one: every control type set from its label, the upload landing and its
re-render not read as a refusal, Submit clicked once under the flag and never
without it, and the follow checkbox unchecked from LinkedIn's checked default.
Two ways the first real application would have failed.

A country list runs to 250 options and the read caps them at 50, so checking
an answer against the options that survived refused every country past the
cut. A list the read reports as cut is left for the browser to match, which is
the only side that has the whole of it; a whole list is still checked here.

A step is re-read after it is answered, and settling on two reads that agree
happens before an upload comes back: LinkedIn only re-renders the step once it
has the file. That left the walk deciding on the step as it stood before the
resume was attached. After an upload it now waits for the step to change.
Measured on a live application: uploading a resume empties the form's resume
list while the file is in flight, and the empty list holds still. Two reads
agree on it, so the walk carried on with a form holding no resume at all, and
under the submit flag that is an application sent with the resume the upload
was meant to replace.

Neither waiting for the step to change nor reading it once it stops changing
sees this. The change fires when the list is torn down, and the torn-down list
is what stops changing. So the file itself is the signal now: LinkedIn names
an uploaded resume on the card it renders for it, and a file it never names
back refuses rather than going out unattached.

The follow checkbox came back checked on the same run, which the same rebuild
explains, and it is now set again if it does not take the first time. Its real
state was, and stays, what the result reports.
It came back checked on two live runs, for two reasons. The retry gave up
without trying anything when LinkedIn left the input without an id, because
the selector it had was keyed on one. And the input is styled invisible behind
its label, which is a control a locator refuses to click rather than clicking
something else on its behalf.

So the box is tried from the most honest way down: check or uncheck on the
input, found by its id or, failing that, as the footer's own checkbox; then
the label, which is what a person clicks and what makes the browser toggle the
box itself. A box that takes neither is still reported as it stands rather
than claimed, because a company in a feed is not worth failing an application
over.
The first real application came back with `confirmation: ""`. It had landed:
the posting showed its applied line and `get_job_details` read it as applied.
So the field said nothing on a submission that worked, and a caller with
nothing else to go on cannot tell that case from a failure.

The dialog closing is what proves the click landed, and that has not changed.
What is read after it is the account of the send, and LinkedIn gives two.
Its own modal is the one this looked for, and it does not always come. The
posting's applied line is the other, and it is not a weaker stand-in: the
modal is LinkedIn narrating the click, while the line is the posting's state
having changed. A posting reading as applied never reaches the form, so the
line cannot have been there before the click.

Both are now polled for a few seconds, the modal first within a round because
it renders immediately. An empty answer no longer reaches the caller at all:
the key is dropped, because a key carrying "" reads as an account of something
and `submitted` is what says the application was sent.

The line is read above the description heading only, through the fragment
`APPLY_SIGNALS_JS` already used for the applied state, so the two readers of
the posting's state cannot come to disagree about where the boundary sits.
A typeahead is answered by naming it with the value to look up. The field
is typed one character at a time, and what lands in it is the suggestion
LinkedIn offered whose visible text is the answer, clicked. An answer no
suggestion wears clears the field and refuses with the suggestions
quoted, so the retry is one call away.

An empty typeahead now refuses only when no answer names it, and a
prefilled one can be replaced by naming it; both refused outright before,
which is what made a wrong saved value unfixable. A prefill that already
is the answer is left alone rather than retyped, so LinkedIn's binding of
that text to an entity survives.

Picks run before every other edit of the step, because a pick only learns
whether it matches after its value has reached the page, and one that
finds nothing has to leave the rest of the dialog as LinkedIn prefilled
it.
The dialog renders inside the open shadow root of div#interop-outlet, so
the document itself sees no dialog, no combobox and no suggestion list at
all; only a locator that pierces the root reaches them. The suggestions
render beside the input inside that root rather than portalled to the
body, the field names its open list through both aria-owns and
aria-controls, and no control anywhere carries the entity behind the
text, which is why a pick can only be confirmed by the value read back
and the list closing.

The synthetic dialog now renders its list where LinkedIn renders it, so
reading options off the document instead of through a locator fails five
tests rather than passing them. Its hidden entity control stays, named
for what it is: a stand-in for state LinkedIn keeps out of the DOM
entirely, kept for the one behaviour both share, that nothing bound is
what Next refuses on.

A city query comes back ranked by the query alone and by nothing about
the member, and its first suggestion is written the way LinkedIn
writes a profile's location, so a value read off a profile names a real suggestion.
A posting whose Easy Apply is a button with no href, which
get_job_apply_url reads as easy_apply since stickerdaniel#1250, opens a redesigned
dialog: a native <dialog> in the document with no data attributes on
Next, Submit or Discard. The form walk only knows the dialog the /apply/
anchor opens, so on a button-only posting it now refuses before opening
anything rather than leave a draft it cannot find to discard.
submit_easy_apply had the read's gap: a cancelled walk raised past
`except Exception` and left the dialog open on the shared page, holding the
answers typed so far as a draft. It now closes the dialog the way the read
does, shielded and bounded, on every exit.
submit_easy_apply raised the draft warning as a plain RuntimeError when the
walk failed and the dialog stayed open, so the section error named
RuntimeError instead of easy_apply_draft_left. It raises
EasyApplyDraftLeftError, as the read does, and the workflow answers it with
the draft section error and nothing sent.
The read now cuts a radio or checkbox group at 50 options, like a select.
A select is matched by the browser, but a choice is clicked through the
control its label stands for, and the labels past the cut were never read.
Answering such a group refuses, naming the cut, instead of failing on
labels and controls that no longer line up.
Since stickerdaniel#1376 the tools share one error envelope, and submit_easy_apply
still spelled its own and imported the names that refactor removed. It
now takes the decorator like the other tools, and the every-tool tests
call it.
An answer that does not fit the field it names is shaped by the same branch
as an invalid reference, so it names `invalid_argument` and the caller can
correct its answers instead of retrying. `_BRANCHES` lists one failure per
branch of `raise_tool_error`, and the refusal was missing from it.
The required-field check skipped every field an answer named, so an empty
text or a lone checkbox answered no passed it: a rehearsal reported the
form complete and a confirmed run clicked Next on a step LinkedIn would
refuse. The step as read back decides now. Only a file answered on the
step stays exempt, because its input never reports the file and the
upload is waited for until the form names it instead.
The answering walk took any text a field described itself with as an
error once Next was clicked. The step read carries hints there too,
since LinkedIn names both through aria-describedby, so a step that moved
onto a question with a hint ended the run as refused and the draft was
discarded. Only a step Next did not move off is judged now, on the
errors the click added, as the read walk does. The steps the walk
returns drop hints the same way get_easy_apply_form's do.
Once Submit closed the dialog the application is sent, but a page read
failing while the confirmation was polled raised out of the walk, and
the tool answered submitted: false. A caller would be right to send it
again. The failure is logged and the confirmation left out instead,
which already says nothing about whether the application went out.
The upload wait took any label containing the file's name as the form
naming it back, so a card for old-resume.pdf stood in for resume.pdf and
the walk carried on before the upload landed. The name now has to stand
on its own in the label.

A card LinkedIn already held under the same name still cannot be told
from the new one: nothing measured on the live dialog says which card is
new or which is selected.
A select the read cut at 50 options was left for the browser to match,
so a label missing from the whole list passed planning and select_option
waited out its timeout, after the step's other answers had gone in. The
caller got issue diagnostics instead of the field's refusal.

The step read now keeps the whole list of a cut select beside the 50 it
returns, and the planner checks the answer against it. The whole list
stays out of the step the caller sees.
The typeahead branch returned its pick before the max_length check the
other typed fields go through, so an answer longer than the field was
typed in, cut by the browser, and refused for matching no suggestion.
It now refuses with the length, while the step is still untouched.
submit_easy_apply asked for a ready extractor before it checked its job id,
so a malformed one could start a browser, install Chromium or spend a login
before the caller saw the refusal. It normalizes the id first, as
get_easy_apply_form and dismiss_job do since stickerdaniel#1429, and joins their case in
test_invalid_reference_is_rejected_before_extractor. The answers are checked
against the form LinkedIn renders, so they stay with the extractor.
@shbernal
shbernal force-pushed the feature/easy-apply-submit branch from d04567d to f816bf2 Compare November 14, 2026 11:25
@shbernal

Copy link
Copy Markdown
Contributor Author

Rebased onto main after #1429 and the Easy Apply test fixes (#1441–#1445). submit_easy_apply now refuses a bad job id before it starts a browser, like get_easy_apply_form, with a case in test_invalid_reference_is_rejected_before_extractor. Its README line is shortened for the new width check. Fork CI passed on a build carrying the new head (f816bf24).

The step read gives a select's selected option text as its value, so a
required select still showing "Select an option" counted as answered: a
rehearsal reported the form complete and a confirmed run clicked on. The
read now flags a selected option that is a placeholder by its markup (an
empty value, disabled or hidden), never by its translated words, and the
required check treats a flagged select as empty. The flag stays private.

The synthetic contact step's placeholder now has an empty value, as a
browser-validated required select needs, and the country test answers the
email it had been leaving on the placeholder.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Answer and submit an Easy Apply form

1 participant